Pitch idea: Implicit Fluent Chaining
Motivation
Builder-style configuration is common in Swift but requires mechanical boilerplate for every settable property:
// value types:
extension S {
consuming func foo(_ v: Int) -> Self {
var copy = self; copy.foo = v; return copy
}
}
// reference types:
extension C {
// reference types can use the value-type form above, or this simpler version:
func foo(_ v: Int) -> Self {
self.foo = v; return self
}
}
This scales linearly with property count — each setter is a real function declaration that emits a symbol in the binary (notably in debug builds, where the compiler can't strip or inline it away) — and the pattern is repeated across SwiftUI-style APIs, configuration objects, and DSLs.
Note: this pitch addresses cases where a memberwise initializer (auto-synthesized or manual) is not available or not suitable — e.g., types with many optional/defaulted or computed properties, reference types (which don't get memberwise inits), types from modules you don't own, or APIs where partial/incremental configuration is the point (à la SwiftUI modifiers). When
S(foo: 1, bar: 2)works and reads well, it remains the right tool.
Proposed Solution
For any settable property foo: T, allow instance.foo(value) at the call site to mean "produce an instance with foo set to value" (as if the above boilerplate were present, although not necessarily by having the associated function stubs present in the resulting binary):
let s: S = S().foo(1).bar(2)
let c: C = C().foo(1).bar(2)
The compiler picks semantics based on the type:
- Value types: consuming copy-and-mutate, returning a copy.
- Reference types: in-place mutation, returning
self.
Nothing is added to the type's declared API surface; the call syntax is enabled directly by the settable property.
Because the synthesized form is pure call-site sugar — s.foo(1).bar(2) lowers directly to the equivalent sequence of assignments — no foo(_:) or bar(_:) function symbol is emitted in the binary at any optimization level. This is an actual difference from the hand-written version, not just an aesthetic one.
Compatibility
If a method foo(_:) callable with the given argument is visible (on the type, an extension, or a conformed protocol), the user-declared method wins unconditionally. The synthesized form only activates when no such method exists.
Alternatives considered
Alternative 1. Do not have this feature
The user site is not too bad:
var s: S = S(); s.foo = 1; s.bar = 2
// tolerate the rest of the scope seeing this variable as "var"
For reference types it's even simpler:
let c: C = C()
c.foo = 1
c.bar = 2
Note that this approach has these drawbacks:
- It will typically not be written on a single line (e.g. due to user coding style guides discouraging semicolons), hence it usually takes more vertical space in source.
- In the case of value types, users will have to tolerate the binding being
varinstead oflet, which is undesirable (because it weakens local reasoning about immutability, signals to readers that the value may be mutated later when it won't be, and prevents the compiler from enforcing that the value stays unchanged after construction). - It's not DRY — the variable name is repeated on every line.
- For more complex cases an extra variable will have to be introduced just to hold the intermediate value, polluting the surrounding scope and forcing the reader to track a name that exists only for setup purposes:
users[id].properties[1] = C()
users[id].properties[1].foo = 1
users[id].properties[1].bar = 2
→
let c = C()
c.foo = 1
c.bar = 2
users[id].properties[1] = c
- As in other alternatives, the use site is heavier and not quite as nice as in the proposal.
Alternative 2. Use Kotlin-inspired apply function
func apply<T>(_ v: T, _ execute: (T) -> T) -> T {
execute(v)
}
Usage:
// value types:
let s = apply(S()) {
var copy = $0
copy.foo = 1
copy.bar = 2
return copy
}
// reference types
let c = apply(C()) {
$0.foo = 1
$0.bar = 2
return $0
}
- The use site is heavier and not quite as nice here as in the proposal.
Alternative 3. Use an ad hoc closure
let s: S = {
var s = S()
s.foo = 1
s.bar = 2
return s
}()
- The use site is heavier and not quite as nice here as in the proposal.
See Also
The idea of this pitch was inspired by the discussion in Calendar init PR, although it by no means replaces that PR, for which the conventional "init" approach seems ideal.