Problem
struct Foo { var items: [Int]? }
var foo = Foo()
Now I want to add something to foo.items
. But because it's optional, I can't do foo.items.append(3)
: I first need to provide a default value of []
.
Workarounds today
Awkwardly long, and the new item is spelled twice:
if foo.items == nil { foo.items = [3] }
foo.items?.append(3)
Awkwardly long, and uses force-unwrap when we know items is non-nil (safe for now, but the compiler isn't helping us keep it safe as the code is refactored):
if foo.items == nil { foo.items = [] }
foo.items!.append(3)
We can hide the awkwardness in a generic operator, but still need a force-unwrap:
(If we could return inout T
from the function, we could do (foo.items ??= []).append(3)
, but that is not supported.)
foo.items ??= []
foo.items!.append(3)
// via:
infix operator ??= : AssignmentPrecedence
func ??=<T>(lhs: inout T?, rhs: @autoclosure () -> T?) {
lhs = lhs ?? rhs()
}
Subscripts allow us to provide both get & set directions, so this is pretty good, but can only be defined by extension Foo
, not in general: (see below for a more general version I found while writing this post)
foo[createIfEmpty: \.items].append(3)
// via:
extension Foo {
subscript<T>(createIfEmpty keyPath: WritableKeyPath<Self, [T]?>) -> [T] {
get {
if let value = self[keyPath: keyPath] {
return value
}
return []
}
set {
self[keyPath: keyPath] = newValue
}
// ...or something like:
_read {
yield self[keyPath: keyPath] ?? []
}
_modify {
if self[keyPath: keyPath] == nil { self[keyPath: keyPath] = [] }
yield &self[keyPath: keyPath]!
}
}
}
Okay, so we can work around this with operators which can be defined as global/free functions... But because we can't return inout T
, the modification has to be wrapped in a closure (unlike subscript
):
foo ~~ (\.items, { $0.append(3) })
infix operator ~~
func ~~<T, U>(lhs: inout T, rhs: (WritableKeyPath<T, [U]?>, (inout [U]) -> Void)) {
if lhs[keyPath: rhs.0] != nil {
rhs.1(&lhs[keyPath: rhs.0]!)
} else {
var newValue: [U] = []
rhs.1(&newValue)
lhs[keyPath: rhs.0] = newValue
}
}
Or perhaps a more ergonomic version with a second operator (if I may be allowed to abuse operator names from Control.Lens.Setter), although it now requires an @escaping
closure:
foo&\.items %~ { $0.append(3) }
precedencegroup ApplyPrecedence { }
precedencegroup ModifyPrecedence { higherThan: ApplyPrecedence }
infix operator & : ApplyPrecedence
infix operator %~ : ModifyPrecedence
func & <T, U>(lhs: inout T, rhs: (WritableKeyPath<T, [U]?>, (inout [U]) -> Void)) {
if lhs[keyPath: rhs.0] != nil {
rhs.1(&lhs[keyPath: rhs.0]!)
} else {
var newValue: [U] = []
rhs.1(&newValue)
lhs[keyPath: rhs.0] = newValue
}
}
func %~ <T, U>(lhs: WritableKeyPath<T, U?>, rhs: @escaping (inout U) -> Void) -> (WritableKeyPath<T, U?>, (inout U) -> Void) {
return (lhs, rhs)
}
And actually, I realized as I was writing this post that there's another nicer solution in just putting the subscript directly on Optional without using KeyPaths:
foo.items[withDefault: []].append(3)
extension Optional {
subscript(withDefault value: @autoclosure () -> Wrapped) -> Wrapped {
get { return self ?? value() }
set { self = newValue }
// or:
_read { yield self ?? value() }
_modify {
if self == nil { self = value() }
yield &self! // can we avoid this force-unwrap? unsafelyUnwrapped can only be used as a getter...
}
}
Better solutions?
So it's clear that the problem can be worked around, but I'd argue most of the solutions above aren't very Swifty, as they're either overly verbose, overly opaque, or use unnecessary force-unwrapping.
The one I do like is extension Optional { subscript(withDefault value: Wrapped) }
. However, I still feel it's limiting: we have to repeat [withDefault: []]
on every access, or use ?.
or !.
each time:
foo.items[withDefault: []].append(2)
foo.items[withDefault: []].append(3) // repetitive
foo.items!.append(4) // prone to breakage if above code is moved
I feel like I'd want to write either:
inout var items = foo.items.withDefault([]) // or just "foo.items ?? []"
items.append(2)
items.append(3)
or
foo.items[withDefault: []].tap {
$0.append(2)
$0.append(3)
}
And these would require language features we don't have: returning inout
, and/or extensions on Any.
Returning inout
I used subscripts above not because this "feels like" a subscript access, but because subscripts allow modifying a value with extra logic before (unwrapping the Optional) and cleanup afterward (re-wrapping). I imagine this is much easier to support for subscripts, whose lifetime is limited to a single statement, than for general long-lived values.
However @John_McCall's Ownership Manifesto does have a section on returning inout
"ephemerals" from regular functions:
… The correctness of this depends not only on the method being able to clean up after the access to the property, but on the continued validity of the variable to which
self
was bound. What we really want is to maintain the current context in the callee, along with all the active scopes in the caller, and simply enter a new nested scope in the caller with the ephemeral as a sort of argument. But this is a well-understood situation in programming languages: it is just a kind of co-routine. …
So that's why yield
is used in _modify
, and it feels natural to do it similarly in a non-subscript function:
extension Optional {
mutating func withDefault(_ value: @autoclosure () -> Wrapped) -> inout Wrapped {
if self == nil { self = value() }
yield &self!
}
}
Now I imagine the hard part is making the compiler correctly track/extend the lifetime of the inout (or coroutine), and the lifetime of the object it's referencing. I'm sure there is some precedent in Rust here, but I don't know it well enough to say. I haven't been following the work on coroutines, but I'm curious if the current plans can help address this.
extending Any
The other natural solution I can see is with something like Ruby's tap
. This can be done with a free function today:
tap(foo.items[withDefault: []]) {
$0.append(2)
$0.append(3)
}
func tap<T>(_ value: T, _ body: (inout T) -> Void) -> T {
var value = value
body(&value)
return value
}
But that feels awkward, especially as we're getting more used to the method-chaining style of SwiftUI modifiers. (And I'm not sure if the extra local var
affects optimization at all.)
(Aside: hmm, I see @beccadax added a TapExpr
to the compiler in #20214 )
I'd prefer this, although I don't know if there are technical reasons you can't extend Any/AnyObject, so I'd be interested to hear if anyone knows more:
foo.items[withDefault: []].tap {
$0.append(2)
$0.append(3)
}
extension Any {
mutating func tap(_ body: (inout Self) -> Void) -> Self {
body(&self) // does this violate the law of exclusivity in a mutating function?
return self
}
}
Note that the subscript is still required in order to extract a non-Optional "inout" ephemeral from the original Optional. But as I mentioned before, I don't think this API feels like it should be a subscript access; I'd rather have foo.items.withDefault([]).tap { ... }
but it isn't possible today as far as I know. So maybe we should allow both returning inout and extending Any?