- Proposal: TBD
- Author: Erica Sadun, Brent Royal-Gordon
- Status: TBD
- Review manager: TBD
This proposal introduces a with
function to the standard library. This function
simplifies the initialization of objects and modification of value types.
Swift-evolution thread: What about a VBA style with Statement?
When faced with modifying a value-type constant or initializing Cocoa objects, many developers find themselves using closure-based solutions. For example, they may duplicate and modify a constant value type using a closure:
struct Person { var name: String, favoriteColor: UIColor }
let john = Person(name: "John", favoriteColor: .blueColor())
let jane: Person = { var copy = $0; copy.name = "Jane"; return copy }(john)
print(jane) // Person(name: "Jane", favoriteColor: UIDeviceRGBColorSpace 0 0 1 1)
Or they may create then customize a Cocoa object:
let questionLabel: UILabel = {
$0.textAlignment = .Center
$0.font = UIFont(name: "DnealianManuscript", size: 72)
$0.text = questionText
$0.numberOfLines = 0
mainView.addSubview($0)
return $0
}(UILabel())
The technique consistently draws interest from frustrated developers, and Swift Evolution and Github are rife with variations on this theme. Taking a broader view, this can be seen as an example of a more general operation: modifying or using a value in passing. There are several other places where this same operation would be useful, especially since the adoption of SE-0003. SE-0003 eliminated var parameters, replacing them with shadowed var copies which must be modified and used.
Using a closure technique like the ones demonstrated above has significant drawbacks:
- Swift can't infer the return type, so you must state it explicitly.
- You must put the call to the instance's initializer after the code that uses it.
- You must explicitly
return
the instance. - If the instance is a value type, you must explicitly assign it to a
var
to make it mutable.
Nevertheless, its benefits are compelling:
- By giving the instance a short, temporary name, it reduces the noise of the longhand form.
- It visually groups together all the setup code for a given object.
We propose to introduce a new global with
function that preserves the
closure approach's benefits while eliminating its drawbacks. Here are
some examples where this function may be used.
Consider the situation where you need a copy-and-return method and
the only available method mutates self
, as in the following example:
// Desired behavior:
let fewerFoos = foos.removing(at: i)
Swift enables you to work around this limitation by creating a var copy, which you mutate:
var fewerFoos = foos
fewerFoos.remove(at: i)
Alternatively, you can embed these steps into a closure and assign the results to a new constant instead of using a variable:
let fewerFoos: Array<Foo> = {
var copy = $0
copy.remove(at: i)
return copy
}(foos)
By introducing with
, you streamline this update, creating a simple
version that drops the explicit return and brings the modified item to
the front of the call:
let fewerFoos: Array<Foo> = with(foos) {
$0.remove(at: i)
}
The Person
example shown earlier in this proposal streamlines down to:
let jane = with(john) { $0.name = "Jane" }
Although these examples use closures with $0
, developers may
use named parameters or pass a named function instead of a closure.
The copy-mutate-and-return pattern is used widely both in the standard library
and in third party code as a result of the acceptance of SE-0003.
Many functions now shadow previous var
parameters in order to mutate them, as in this RangeReplaceableCollection code:
public func +<
RRC1 : RangeReplaceableCollection,
RRC2 : RangeReplaceableCollection
where RRC1.Iterator.Element == RRC2.Iterator.Element
>(lhs: RRC1, rhs: RRC2) -> RRC1 {
var lhs = lhs
// FIXME: what if lhs is a reference type? This will mutate it.
lhs.reserveCapacity(lhs.count + numericCast(rhs.count))
lhs.append(contentsOf: rhs)
return lhs
}
Our proposed rewrite looks like this:
public func +<
RRC1 : RangeReplaceableCollection,
RRC2 : RangeReplaceableCollection
where RRC1.Iterator.Element == RRC2.Iterator.Element
>(lhs: RRC1, rhs: RRC2) -> RRC1 {
// FIXME: what if lhs is a reference type? This will mutate it.
return with(lhs) {
$0.reserveCapacity($0.count + numericCast(rhs.count))
$0.append(contentsOf: rhs)
}
}
The FIXME
comment in raises an important issue about reference types.
In this example, as with our proposed solution, you must take care with
reference types. Swift does not offer a native copy
for reference types.
Using copy-and-mutate changes the original when used with references.
Mutation isn't an issue when limiting with
to NSObject object set-up or
modifying an NSObject instance created by copying. When using native Swift,
you must supply your own class-based copying or you may mutate the passed
instance.
Suppose you want to inspect a value in the middle of a long method chain You're not sure this is retrieving the type of cell you expect:
let view = tableView.cellForRow(at: indexPath)?.contentView.withTag(42)
To log the cell, you must split the statement in two:
let cell = tableView.cellForRow(at: indexPath)
print("Got cell \(cell)")
let view = cell?.contentView.withTag(42)
Or use a closure workaround:
let cell = tableView.cellForRow(at: indexPath)
print("Got cell \(cell)")
let view = ({
print("Got cell \($1)")
return $1
}()?.contentView.withTag(42)
Our proposed solution simplifies this to the following parsimonious and chainable solution:
let view = with(tableView.cellForRow(at: indexPath)){ print($0) }?.contentView.withTag(42)
This updated version is more fluent and Swift-like than the previous workarounds.
After creating a Cocoa instance, you must often set several properties on it. Writing this in the most obvious, straightforward style results in a lot of repetition and visual constipation.
Using with
streamlines the questionLabel
example from earlier in
this proposal to:
let questionLabel = with(UILabel()){
$0.textAlignment = .Center
$0.font = UIFont(name: "DnealianManuscript", size: 72)
$0.text = questionText
$0.numberOfLines = 0
mainView.addSubview($0)
}
This proposal introduces with
, a function that accepts a value of T
and
a closure of (inout T) -> Void
. with
assigns the value to a new
variable, passes that variable as a parameter to the closure, and
then returns the potentially modified variable. That means:
- When used with value types, the closure can modify a copy of the original value.
- When used with reference types, the closure can substitute a different
instance for the original, perhaps by calling
copy()
or some non-Cocoa equivalent.
The closure does not actually have to modify the parameter; it can merely use it, or (for a reference type) modify the object without changing the reference.
This proposal adds with(_:update:)
to the standard library
as an ordinary free function:
@discardableResult
public func with<T>(_ item: T, update: @noescape (inout T) throws -> Void) rethrows -> T {
var this = item
try update(&this)
return this
}
@discardableResult
permits the use of with(_:update:)
to create a scoped
temporary copy of the value with a shorter name.
This proposal is purely additive and has no impact on existing code.
Doing nothing: with
is a mere convenience; anything written with it could be
written another way.
If rejected, users could continue to write code using the longhand form,
the various closure-based techniques, or homegrown versions of with
.
Using method syntax: Some list members preferred a syntax that looked more like a method call with a trailing closure:
let questionLabel = UILabel().with {
$0.textAlignment = .Center
$0.font = UIFont(name: "DnealianManuscript", size: 72)
$0.numberOfLines = 0
addSubview($0)
}
This would require a more drastic solution as it's not possible to add
methods to all Swift types. Nor does it match the existing
design of functions like withExtendedLifetime(_:_:)
, withUnsafePointer(_:_:)
,
and reflect(_:)
.
Adding self
rebinding: Some list members wanted a way to bind
self
to the passed argument, so that they can use implicit self
to
eliminate $0.
:
let supView = self
let questionLabel = with(UILabel()) {
self in
textAlignment = .Center
font = UIFont(name: "DnealianManuscript", size: 72)
numberOfLines = 0
supView.addSubview(self)
}
We do not believe this is practical to propose in the Swift 3 timeframe, and
we believe with
would work well with this feature if it were added later.
Adding method cascades: A competing proposal was to introduce a way to use several methods or properties on the same instance; Dart and Smalltalk have features of this kind.
let questionLabel = UILabel()
..textAlignment = .Center
..font = UIFont(name: "DnealianManuscript", size: 72)
..numberOfLines = 0
addSubview(questionLabel)
Like rebinding self
, we do not believe method cascades are practical
for the Swift 3 timeframe. We also believe that many of with
's use
cases would not be subsumed by method cascades even if they were added.