I agree. It is the responsibility of the API vendor to carefully choose the scope. We have a simple example with the with(value) { ... } function. This one is very local, very easy to understand.
The GRDB example is interesting, because it exemplifies how the scope can be different than a direct function argument. But it is still very carefully chosen:
In the code below, the request returned by Player.all() is generic on Player. Its filter(_:) method has an autoclosure argument which is scoped on Player.Type. This allows the user to define the Player static members that are granted with leading dot syntax:
// Library code (simplified)
protocol Table {
static func all() -> Request<Self>
}
struct Request<T: Table> {
func filter(_ expression: @autoclosure @scoped (T.Type) -> Expression) { ... }
}
// User code
struct Player: Table {
static let name = Column("name")
}
let request = Player.all().filter(.name == "foo")
// ^...................~~~~~
See how it happens that the API vendor knows where the user is most likely to define the identifiers that deserve the leading-dot syntax. In the case of GRDB, it is natural that a type defines its own columns as static members. The documentation will guide the user to this natural sweet spot.
Some users willl prefer to define columns in some other places, and it's 100% ok. But they may end up preferring static properties for more streamlined code:
struct Player: Table {
enum Column: String { case name }
}
let request = Player.all().filter(.Column.name == "foo")
This won't hold in the general case though. Imagine I'm vending a generic function over T (point being that I don't know anything about T ) that wants an argument of type (T, T) -> () [...] if I make the closure parameter scoped over, say, the first parameter, then it's really only 50/50 chance that I correctly guess that users would want to access the first argument more often than the second one and thus would like to have syntactic conveniences with that particular argument.
Then let the user scope on his preferred one?
func doSomethingGeneric<T>(_ closure: (T, T) -> ()) { ... }
doSomethingGeneric { a, b in
with(a) { // if the user wants to scope, she scopes
print(.foo) // a.foo
print(b.foo) // b.foo
}
}
Autoclosures [...] are defined to be an attribute that is applied to "a parameter whose type is a function type that takes no arguments ", so the autoclosure semantics would need to be extended at least.
This is exact. GRDB needs scoped autoclosures. I'm aware those won't be easy to deal with (both in the compiler, and with language lawyers).
Here I have to count on the will to improve the language. Scoped auto closures are a boon that would help GRDB, but not only. They could have solved the SE-0299 Problem:
// In SwiftUI
extension Toggle {
func toggleStyle(_ style: @autoclosure @scoped (ToggleStyles.Type) -> ToggleStyle)
}
enum ToggleStyles {
static let switch: ToggleStyle = SwitchToggleStyle()
...
}
// In applications
Toggle("Wi-Fi", isOn: $isWiFiEnabled)
.toggleStyle(SwitchToggleStyle()) // Still OK
.toggleStyle(.switch) // New!
I will look for more examples.
Going back to my earlier argument: I believe that it's the user who should decide how they want to write the closure definition, not the API that takes the closure. To be more concrete, here's how I would rewrite one of your examples [...]
I think this pitch, with the help of the with function, addresses your need.
By the way, your messages are very helpful, thank you!