Pitch: Scoped Functions
Scoped Functions are functions that enhance the leading dot syntax inside their body, so that it gives short-hand access to the members of a specific value. Such functions exist so that API designers and their consumers can collaborate towards a way to write focused code where an agreed implicit and obvious context does not have to be spelled out.
For the posterity, the initial pitch is below.
Initial pitch
Introduction
Scoped Functions are functions that enhance the leading dot syntax inside their body, so that it gives short-hand access to the members of a specific value. Such functions exist so that API designers and their consumers can collaborate towards a way to write focused code where an agreed implicit and obvious context does not have to be spelled out.
Motivation
This pitch considers that developers often want to use short-hand identifier resolution in particular contexts, and api designers often want to make it possible.
The Swift language already provides much help in this regard. We have the implicit self
, the current (and evolving) rules for the leading dot syntax, the scoping rules for accessing nested types, etc.
But developers always want more:
- SE-0299 has revealed a way to better control the scope of the leading dot syntax.
- Several threads in the Swift Forums request a Kotlin feature named Type-safe builders, the latest being Can I make certain functions available within a closure arg?.
- Several threads also request a feature where members of a variable can be configured without repeating the name of the variable.
- (Insert other examples here)
In all those examples, developers express a desire to enhance identifier resolution in a lexically scoped context: a value, or the body of a closure, where both are arguments of a function.
Considering:
- The braces of a closure body are a well delimited lexical scope.
- A function call is a strong semantic signal that is able to define an implicit and obvious context that both api designers and consumers can agree on.
- Naked identifier resolution in Swift is already pretty busy with local variables,
self
, type names, module names, etc, so we do not propose modifying this. - The leading dot syntax is a beloved short-hand syntax of Swift.
We modify the rules that govern leading dot syntax inside the body of a particular closure argument. When the closure is an autoclosure, the leading dot syntax is modified for the value that feeds this autoclosure.
This gives:
// The build(_:configure:) function accepts a closure
// scoped on its first argument:
let label = build(UILabel()) {
.text = "Hello scoped functions!"
.font = .preferredFont(forTextStyle: .body)
}
// The `filter(_:)` and `order(_:)` functions accept an
// autoclosure scoped on the generic type of the request
// (here, `Player`).
// `Player` collaborates, and has defined two
// `Player.score` and `Player.team` static constants.
let players = try Player.all()
.filter(.team == "Red")
.order(.score.desc)
.limit(10)
.fetchAll(from: database)
// A mix and match between result builders and
// scoped functions. Each closure is scoped on an
// object that defines the relevant leading-dot apis
// inside the body.
let html = HTML {
.head { .title("Hello scoped functions!") }
.body { ... }
}
// A revisit of SE-0299: the argument of toggleStyle(_:) is
// an autoclosure scoped on an ad-hoc enum type:
Toggle("Wi-Fi", isOn: $isWiFiEnabled)
.toggleStyle(.switch)
Without scoped functions, developers face various problems:
// The `label` variable is repeated.
let label = UILabel()
label.text = "Hello World!"
label.font = .preferredFont(forTextStyle: .body)
// The `Player` type is repeated.
let players = try Player.all()
.filter(Player.team == "Red")
.order(Player.score.desc)
.limit(10)
.fetchAll(from: database)
// Typesquatting: the `Head`, `Title`, `Body`
// identifiers can't be used for other purposes.
let html = HTML {
Head { Title("Hello World!") }
Body { ... }
}
// As before SE-0299
Toggle("Wi-Fi", isOn: $isWiFiEnabled)
.toggleStyle(SwitchToggleStyle())
Personal motivation: as the author of GRDB, which defines a type-safe API for building SQL queries, and relies of static properties defined on user-defined "record types", I'd love to simplify some user-land code:
// CURRENT GRDB CODE struct Team: Codable, FetchableRecord { static let players = hasMany(Player.self) static let awards = hasMany(Award.self, through: players, using: Player.awards) } struct Player: Codable, FetchableRecord { static let awards = hasMany(Award.self) } struct Award: Codable, FetchableRecord { } // All red teams with their awarded players let request = Team .filter(Team.Columns.name == "Red") .including(all: Team.players.having(Player.awards.count > 0)) // WITH SCOPED FUNCTIONS struct Team: Codable, FetchableRecord { static let players = hasMany(Player.self) static let awards = hasMany(Award.self, through: players, using: .awards) // <- } struct Player: Codable, FetchableRecord { static let awards = hasMany(Award.self) } struct Award: Codable, FetchableRecord { } // All red teams with their awarded players let request = Team .filter(.name == "Red") // <- .including(all: .players.having(.awards.count > 0)) // <-
Sorry if I provided a contrieved example. But contrieved examples are precisely those where unnecessary clutter obscures the intent the most.
Proposed solution
The syntax for defining the type of a function is extended. When the arguments list is prepended with a type T
and a dot, as in T.(...) -> ...
, then this is the type of a scoped function, and T
is the scope type. Such a scoped function accepts an extra first argument, of type T
, which is the scope value. Inside the body of the function, the scope value feeds the leading dot syntax. The function is said to be T
-scoped.
For example:
let f: String.() -> Int // A
f = { return .count } // B
f("Hello") // C: prints "5"
-
In (A), f is defined as a String-scoped function.
-
In (B), the compiler is able to resolve
.count
asString.count
. -
In (C), the function is called with a particular String.
There is no syntax for defining a scoped function from scratch, as in func f T.(...) -> ... { ... }
. Scoped functions are designed to be used as closure arguments, and not free functions or methods. The functions that accept scoped functions as arguments define the "agreed, implicit, and obvious context" that is key for this pitch.
We are able to address some of the above examples:
-
The
build(_:_:)
functionfunc build<T>(_ value: T, with transform: T.() throws -> ()) rethrows -> T { return transform(value) } let label = build(UILabel()) { .text = "Hello scoped functions!" .font = .preferredFont(forTextStyle: .body) } label.text // "Hello"
Woot, we'll have to define how the compiler deals with leading dot syntax for
.font
,.preferredFont
, and.body
:-) -
The database query
struct Request<T> { func filter(_ expression: @autoclosure T.Type.() -> Expression) -> Self { ... } func order(_ ordering: @autoclosure T.Type.() -> Ordering) -> Self { ... } func limit(_ limit: Int) -> Self { ... } } extension Player { static let team = Column(...) static let score = Column(...) } let request: Request<Player> = ... request.filter(.team == "Red").order(.score.desc)
The
@autoclosure
qualifier has the compiler change the leading dot scoping for values.
Detailed design
TBD
Source compatibility
Scoped function are an additive feature that creates no source compatibility issue.
Effect on ABI stability
A scoped function, at runtime, is a plain function that accepts its scope as the first argument. Its a compiler-only feature, without any effect on the ABI.
Effect on API resilience
TBD
Alternatives considered
Use another sigil than .
? For example, '
:
// The build(_:configure:) function accepts a closure
// scoped on its first argument:
let label = build(UILabel()) {
'text = "Hello scoped functions!"
'font = .preferredFont(forTextStyle: .body)
}
// The `filter(_:)` and `order(_:)` functions accept an
// autoclosure scoped on its receiver type.
// `Player` collaborates, and has defined two
// `Player.score` and `Player.team` static constants.
let players = try Player
.filter('team == "Red")
.order('score.desc)
.limit(10)
.fetchAll(from: database)
// A mix and match between result builders and
// scoped functions. Each closure is scoped on an
// object that defines the relevant leading-dot apis
// inside the body.
let html = HTML {
'head { 'title("Hello scoped functions!") }
'body { ... }
}
// A revisit of SE-0299: the argument of toggleStyle(_:) is
// an autoclosure scoped on an ad-hoc enum type:
Toggle("Wi-Fi", isOn: $isWiFiEnabled)
.toggleStyle('switch)
The latest version of the pitch is at Pitch: Scoped Functions · GitHub