Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Null-coalescing assignment operator in Swift 3

Tags:

swift

swift3

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?

like image 715
Gregory Higley Avatar asked Mar 11 '23 23:03

Gregory Higley


2 Answers

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
like image 120
Martin R Avatar answered Mar 13 '23 12:03

Martin R


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.

like image 27
Mark A. Donohoe Avatar answered Mar 13 '23 14:03

Mark A. Donohoe