Implicit parameters...?

[edited 2023-09-27 07:36 based on @ktoso's comments below]

Scala has the concept of implicit values. When I first came across this, I was horrified by what I saw as an overly magical and opaque language feature. It's taken me awhile, but I've come to really appreciate it, especially when it comes to injecting mockable dependencies.

I'm imagining a similar concept in Swift, where the most recent in-scope value will automatically be passed in to implicit functions.

func foo(bar: Int, baz: implicit String) {
  print(1, baz)
}

implicit let baz = "hello"
foo(bar: 1) // "1 hello"
protocol Assistant {
  func doTheThing()
}

final class ViewModel {
  func doTheThing(assistant: some implicit Assistant) {
    assistant.doTheThing()
  }
}

struct ZhuLi: Assistant {
  var season: Int

  func doTheThing() {
    print(season < 4 ? "Yes sir" : "No, Varrick")   
  }
}

implicit let assistant = ZhuLi(season: 4)
let viewModel = ViewModel()
viewModel.doTheThing() // "No, Varrick"

This could make dependency injection a whole lot easier.

I don't currently have the skills to implement anything like this, but I wanted to get folks' thoughts on it (and maybe poke the curiosity of someone who might have more experience than me :sweat_smile:).

5 Likes

I believe there's been other discussions that accomplished something like this which allowed passing a lone label as shorthand for "use the in-scope value with the same name for this argument" e.g.:

foo(bar: 1, baz:)

I've generally liked this direction more than completely implicit alternatives.

4 Likes

I've said before in those conversations that it's somewhat sad to me that English doesn't have an iteration mark like γ€… in Chinese and Japanese, which would function like a horizontal "ditto". It would be a natural fit for such use cases.

8 Likes

I think this becomes a problem as a codebase evolves, which is one of the biggest reasons I like implicit parameters. If a new dependency is added to a project and is used in several places, it becomes tedious to have to modify every call site to take in the new dependency, as opposed to declaring it once at the top level and allowing it to propagate down the dependency hierarchy automatically.

As a real example, in a Scala codebase I worked on recently, we added an implicit Context parameter to hundreds of methods in hundreds of files, but we only had to declare it once at the app entry point, along with a mock in the testing entry point, and so we were able to prevent thousands of lines of code churn.

2 Likes

If you need only to propagate a value implicitly down through the stack and you are ok to sacrifice compile time safety - it's really easy. Just use TLS of some sort. The hard parts are:

  • Propagate an implicit somewhere that will be invoked in another call stack (i.e. on another runloop tick)
  • Do static analysis to prove correctness (i.e. you didn't forget to pass the value at the begging). This is really difficult to do outside the compiler.

There's also the shadowing problem - you have to be ready that some frame may want to define it's own value for an implicit for nested invocations, and the old value has to be restored upon leaving that frame. But that's also relatively easy (TaskLocal has API for that for example).

1 Like

I've not encountered this before. It's intriguing, but because it's unfamiliar I'm not immediately coming up with a compelling (real-world) example for its use in Swift. Is there one (or more) that folks can suggest?

e.g. I'm doing a lot of SwiftUI right now, and there's plenty of boilerplate associated with that - could this help there?

I'm definitely in favour of solving the configuration: configuration problem, that @Jumhyn already noted.

As the resident ex-Scala person I guess I should chime in here :wink:

Let's slightly adjust the snippet we're talking about because implicits must be opt in for the values. It's not like any arbitrary value of the type is picked up for an implicit parameter, only implicit ones are.

implicit let assistant = ZhuLi(season: 4) // implicit is necessary
let viewModel = ViewModel()
viewModel.doTheThing()//(assistant: assistant)

// func doTheThing()(implicit Assistant)

And yes they're one of the best things in Scala tbh. They're excellent for passing execution contexts around, and they're type-safe -- I know I MUST be provided a value of assistant in order to compile.

The same doesn't hold true with task-local values which are "it may be there, or not".

So yeah, I do think implicit parameters are really great and weirdly enough they'd help us with task executors where we could have the implicit Executor to Task decide where a task should execute.

This would then automatically work for Task()(implicit Executor) rather than the specialized compiled feature of @_inheritActorContext, but I'm unsure how the core team feel about such extension to the language.

So the typical use case in Scala (and literarily taken from Akka tbh, this is how actor context and stream materializer work):

protocol Actor { 
  implicit var executor: ... { get }
}

extension Task {
  init(...)(implicit Executor) { ... }
}

would allow for:

actor Kappa { 
  func test() {
    Task()//(self.executor)
    { /* runs on actor's executor */ }
  }
}

Now the problem with that is, if it were to exist, it doesn't integrate with nonisolated. Because Swift leans in heavily into concurrency as part of the type system we do actually a bit more than implicit parameters for execution context would allow. So it's not all roses here, but it's definitely a nice tool that would be nice to have.

Other common uses include "tolerance" for comparing floating point values, or "timeout value" in a context that defaults to a value from scope or can be overriden:

implicit let timeout = Duration.seconds(1)
expectMessage() // timeout
expectMessage()(timeout + .seconds(2))

or an implicit file system for mocking it out easily etc. etc.


Parameters themselfes are a very clean solution in my mind, and get un-necessarily bad rep from outside of the Scala community (it is implicit conversions that are the work of the devil).

8 Likes

We use it a lot for all sorts of things. Primarily in business logic. Basically if you have a call tree with depth >= 3 where you need to propagate >= 2 arguments - it's a candidate for implicits. To name a few things: analytics, metrics, app theme, settings, loggers, language, country, l10n, a bunch of system APIs (app state, bluetooth, pip, notifications, audio session, etc), networking, credential storage, authentication component, internal url handlers... it's only common things in the app start point. There's also a bunch of components that are specific to our apps. And then components define their own implicits for thier nested components, and so on.

5 Likes

Especially things like Environment from SwiftUI would IMO be a really good candidate for implicit parameters. This is better than reflection which is a very implicit form of injection, not to mention it’s slower and less optimizable.

This seems a bit dangerous to me. From what I am understanding, a function can be defined with a parameter named x and if a variable named x is declared before calling that function, the function will
Implicitly take that x value?

So if I redefine a function later that happens to have an implicit argument named β€œname” and I just so happen to have a variable named β€œname” before that function, it could implicitly take that value, with out warning? Kinda feels like a security issue β€” sort of?

I think it would need to be required that the label be included just to signal to the user that something sugary or magical is about to happen, but at that point, needing the implicit behavior seems unneeded.

Regardless interesting feature but -1 for me.

4 Likes

There's a study discussing massive negative impacts on Scala project build times from implicit parameters + implicit conversions, so I think a prerequisite to this being seriously considered should be proving that implicit conversions are the sole cause of those issues in Scala and/or that it can be implemented in the Swift compiler without major impacts to build times.

Here are a few code snippets that adding this feature might permit. Some of them are straw men, but hopefully it's clear that this can be abused in a wide variety of ways, as currently specced.

func foo(baz: implicit String = "lol") { // writing foo() always compiles, but has different meanings depending on scope
  print(1, baz)
}

// let baz = "not funny"
foo() // still builds despite the above being commented out, because of the default parameter

let baz: String? = ""

// later
if let baz {
    foo()  // it looks like we're not using this, but we are
} 

let baz = "declared in the outer context"
runEscapingClosure {
    foo() // baz is in the outer context, but it is visible in the lexical scope, so now it's escaping. For a class that might be a reference cycle, or otherwise have surprising consequences
}

class C<T> {
     let t: T 
     init(`self`: implicit T) { // `self` is totally legal as a parameter name in Swift, and I can think of many potential reasons to want to use it with `implicit`
         t = self // this is also valid Swift
     }
}

class E {
    
    let a: Any = {    
        C() // this would compile. But the value of `self` might be surprising. Without implicits, it might be more obvious that it probably isn't what the author intended
    }()

    var a: Any?
    func cacheSomething() {
         a = C() // reference cycle
    }

}

@propertyWrapper 
struct S<Self> {
     init(wrappedValue: Something, `self`: implicit Self) { ... }
}

class C {

     func f(@S foo: Something) { // there's a `self` in scope here, probably...
           f(foo: something) // ...but it might not the same `self` that's in scope now
     }

     @S  // You'd hope that this would be a good way to pass your instance in so you can implement property wrappers like UIKit's `Invalidating`, but this won't work, because, again, `self` either isn't available, or is your type, not your instance, which will probably lead to some very confusing compiler error messages 
     var myVar 

}

actor DarthVader {

     let executor = SuperStarDestroyer() // Darth Vader's personal star destroyer is named the Executor

    func pursueTheMilleniumFalcon() {
        Task { // compiler error: executor isn't an `Executor` anymore. We can't use Structured Concurrency anymore, and Lord Vader is demanding an update

         }
    }
}

I realize these examples are somewhat contrived, but they show that this feature has potentially unexpected behaviors when composed with other, existing Swift features.

With that said, I do think this is solving a real problem, I'm just skeptical that it's worth the cost in potential compile time performance regression, and odd interactions with existing features.

7 Likes

There's the ditto mark, used to imply "same as above", mostly used in handwritten lists. When e.g. signing into a visitation protocol with columns for date, name and home town, visitors coming as a group will often sign ditto - name - ditto, to indicate same-date, their name, same-town.

I have never seen it used in non-handwritten lists, though. Is this different for the Chinese ditto-mark? Will computer spreadsheets and lists when localized to Chinese or Japanese present identical content with a symbol rather than the actual cell content?

Indulging in the side-thread a little bit...

The japanese γ€… exists in order to simplify writing, because in words where a kanji is repeated exactly the same, it is annoying to have to draw the entire thing again, thus γ€… was introduced. E.g. in words like 色々γͺ – iro-iro-na – "various" it would be annoying to write 色色γͺ so the repeater (no-ma, γ€…, is used). It is the correct way to write those words; and nope, it is not used in other contexts (spreadsheet or what not -- at least I've never seen it in such contexts).

In a programming context I guess _ is always a nice placeholder, but the power of implicits is not having to change call-sites when adding more implicit parameters, so not sure if that'd be nice or not. But I guess it could be a conceivable middle ground...? Not sure :thinking:

In Scala, explicitly spelling out the "summoning" of the value is done by val a: A = implicitly[A] to get a value of A if available in implicit scope; so one can summon and pass around explicitly: test()(implicitly[A]) etc, though that's now how those are used usually; they're more for summoning a type-class implicitly[TypeClass[A]] that we know must exist...

3 Likes

To me, this makes the code almost impossible to understand without and IDE (e.g. when doing code review). Without checking every single function signature I can't be sure some parameter is not implicitly passed to functions what should't have access to that.

6 Likes

I also see complete omission of labels going against readability and clarity of the code. And the example with added implicit Context in one place and materialized to hundreds of methods in hundreds of files sounds rather scary than motivational to me lol. I like the concept though.

A good compromise is spelling the label with some keyword (like the one Xiaodi mentioned) for explicit pass, and as long as the rule will be that implicit parameter label and most recent in-scope value name must match.

That might change the concept from implicit parameters (also implicit parameter modifier keyword becomes unnecessary IMO) to just shorthand-like feature.

foo(bar: 1, baz: `)
foo(bar: 1, baz: _)
foo(bar: 1, baz: $)

To me it looks like a no brainer and just a matter of finding the right keyword for passing. I can imagine IDE to autocomplete with "implicit passing" like this if it finds the variable in scope.

3 Likes

Let's clarify this a bit. Half the people in the thread are talking about (1) automatically substituting a value into the argument of the called function when it is called the same as this argument. The other half is about (2) implicitly passing an argument, which is not specified at all when calling a function.

In both situations when you're reviewing the code you do see what function uses. You don't see how parameters are passed in (2), but that's the compiler's(or a static analysis tool) responsibility to check that. You should not rely on the acuity of the human eye in this matter.

About types of proposed solutions.
Type 1 does help a bit, but only a bit. We would still see this meaningless noise of dumb parameter passing. And diff would still be like

coolFunction(
-   a: _
+   a: _,
+   b: _
)

And type 2 also has its problems. Type of an implicit and even a pair of (name, type) is not always universally define a meaning and scope of application of a value. Like tag: String - what it even means is might be broadly understood by different functions. So sometimes you have to specify a meaning by inventing dedicated "keys" in implicit namespace.
Moreover, type 2 has two subtypes:
A. You specify an implicit parameter in function's signature, like in Scala. I.e. func log(@implicit tag: String)
B. You extract an implicit parameter in function's body (the way we do).

func log() {
  @Implicit(\.logTag) var tag: String
  ...
}

We use type B because it's possible in Swift today, but it implies a number of restrictions. For example, you can't use implicits in functions that can't be proven to be statically dispatched (without optimizer's devirtualization). And of course you can't do static analysis in precompiled swift modules, because you don't have access to functions' bodies.

2 Likes

Thank you for chiming in, @ktoso! I especially want to call out the first part of your post, as it corrects a huge omission on my part.

I think requiring that the implicit keyword is used for parameters that might be used in scope will help a ton with code readability as well as, I imagine, build time.

Thank you for calling this out. It's definitely my intention that we are discussing this sort of feature:

func execute(context: implicit Context) {}

implicit let context = Context()
execute() 

...and not this:

func execute(context: Context) {}

let context = Context()
execute(context:)

The latter saves a couple of characters for each parameter used, but in a rapidly evolving codebase, still leads to the same arity of code churn as it does without such a feature, whereas the former has the potential to eliminate entire categories of code churn.

2 Likes

I agree that having the implicit marking on the caller side makes this more palatable than a truly completely invisible feature at the use site. I still don't love that the places affected by a change to an implicit value are totally invisible to a reader. Of course, I say all this having no direct experience with the inspiring feature.

I'd be curious to know if the goal here is to solve the churn itself (e.g., it pollutes git blame), or the work required for that churn. If more the latter, it strikes me that this is mostly mechanical, i.e., a refactoring tool could have its own "insert implicit parameter" functionality which inserts the label at all appropriate use sites, sidestepping the need for manual intervention.

3 Likes

How does this work with overloads? e.g.:

extension String {
    func log(@implicit configuration: LogConfiguration) { … }
    func log(fileHandle: FileHandle = .standardError) { … }
}

…

"Oh oh".log()

How does the compiler know which method should be used? How does a human know which method is used?

2 Likes