I'm trying to create a null-coalescing assignment operator in Swift 3. In other words, instead of this:
x = x ?? y
I want this:
x ??= y
Swift 3 does not seem to like my operator. Here's its definition:
infix operator ??= : AssignmentPrecedence
func ??=(lhs: inout Any?, rhs: @autoclosure () -> Any?) {
if lhs != nil { return }
lhs = rhs()
}
var x = 3
x ??= 7 // Cannot convert value of type 'Int' to expected argument type 'inout Any?'.
I've done it without the @autoclosure
too. My understanding is that the precedence group AssignmentPrecedence
already contains the assignment
declaration, so this is unlikely to be the problem.
How can I do this in Swift 3?
First, make the operator generic instead of using Any
:
infix operator ??= : AssignmentPrecedence
func ??=<T>(lhs: inout T?, rhs: @autoclosure () -> T?) {
if lhs != nil { return }
lhs = rhs()
}
Second, the left operand needs to be an optional (otherwise it
could not be tested against nil
):
var x: Int? = 3
x ??= 7
I came up with my own 'flavor' of a null-coalescing assignment operator, similar to the above, but by adding a return type, you can both do assignment and return at the same time which is more in line with normal '??' behavior, with the addition of the assignment from the RHS to the LHS if LHS is null. It's perfect for resettable lazy vars. Here's my version...
// Null-coalescing assignment operator
infix operator ??= : AssignmentPrecedence
@discardableResult
func ??= <T>(lhs:inout T?, rhs:@autoclosure () -> T) -> T {
if let lhs = lhs {
return lhs
}
let rhsResult = rhs()
lhs = rhsResult
return rhsResult
}
With the above, I can now do resettable lazy vars like this...
private var qCache:Int?
var q:Int {
return qCache ??= {
print("Lazy-calculating q...")
return 44
}()
}
func resetQ() { qCache = nil }
print("1: q is \(q)")
print("2: q is \(q)")
print("3: q is \(q)")
print("Resetting lazy q...")
resetQ()
print("4: q is \(q)")
Output...
Lazy-calculating q...
1: q is 44
2: q is 44
3: q is 44
Resetting lazy q...
Lazy-calculating q...
4: q is 44
You can also implement the setter too, if you wish as well as separating out the 'lazy calc' function...
func lazyCalcQ() -> Int {
print("Lazy-calculating q...")
return 44
}
private var qCache:Int?
var q:Int {
get { return qCache ??= lazyCalcQ() }
set { qCache = newValue }
}
func resetQ() { qCache = nil }
Going a step further, you can use implicitly-unwrapped datatypes so you can use the assignment of nil
to be the reset, yet you are always guaranteed a value from the getter. The only down-side is you still sometimes have to force-unwrap it to silence some warnings like in the print statements below, but again, you are guaranteed a value, so it's safe to do.
Note: Personally I prefer the approach above with the non-implicitly-unwrapped version and the extra 'reset' function--especially if it's a read-only property--because it makes the API much clearer, but I'm sharing this for completeness since it shows a creative use for nil-as-reset.
func lazyCalcR() -> Int {
print("Lazy-calculating r (i.e. the default when reset with 'nil')...")
return 10
}
private var rCache:Int?
var r:Int! {
get { return rCache ??= lazyCalcR() }
set { rCache = newValue }
}
print("1: r is \(r!)")
r += 10
print("2: r is \(r!)")
r += 10
print("3: r is \(r!)")
print("Resetting r to default...")
r = nil
print("4: r is \(r!)")
Output...
Lazy-calculating r (i.e. the default when reset with 'nil')...
1: r is 10
2: r is 20
3: r is 30
Resetting r to default...
Lazy-calculating r (i.e. the default when reset with 'nil')...
4: r is 10
Of course the above are all trivial examples using an int, but I use it to do things like calculating complex paths based on bounds, etc.
My next attempt will be to hide all of this in property wrappers, eliminating the need for this operator itself.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With