Defining an operator for optional types

I’m having a very hard time googling for an answer to this issue I ran into. I wanted to create an optional assignment operator that would make expressions like this more compact:

foo = updatedFoo ?? foo

It looks like this:

protocol
OptionalAssignable : Any
{
	static func ??=(lhs: inout Self, rhs: Self?)
}

infix operator ??= : AssignmentPrecedence

extension
OptionalAssignable
{
	static
	func
	??=(lhs: inout Self, rhs: Self?)
	{
		if let v = rhs
		{
			lhs = v
		}
	}
}

extension Float : OptionalAssignable {}
extension Int : OptionalAssignable {}
extension String : OptionalAssignable {}

This works well, when the types are non-optional, and I can write some database update code that looks like this:

	furnace?.number ??= furnaceUpdate.number
	furnace?.name ??= furnaceUpdate.name
	furnace?.minTemp ??= furnaceUpdate.minTemp
	furnace?.maxTemp ??= furnaceUpdate.maxTemp
	furnace?.idleTemp ??= furnaceUpdate.idleTemp
	furnace?.gas1Name ??= furnaceUpdate.gas1Name
	furnace?.gas2Name ??= furnaceUpdate.gas2Name

Some of those are Int, some are Float, but unfortunately, some are String?`, and the compiler gets mad at me for those, saying

referencing operator function '??=' on 'OptionalAssignable' requires that 'String?' conform to 'OptionalAssignable'

I had hoped it would just figure it out. I tried to make an overloaded ??= that took two Self? arguments, but then I get "member operator '??=' of protocol 'OptionalAssignable' must have at least one argument of type 'Self'."

I tried to solve it by adding a conformance for Optional, but that not only feels…unethical, it doesn’t work, as the operator definition ends up being ??=(lhs: inout Optional, rhs: Optional?), and the test fails.

Update:

Am I missing a better approach?

2 Likes

I would make the operator a generic function like this:

infix operator ??= : AssignmentPrecedence

func
??=<T>(lhs: inout T, rhs: T?)
{
    if let v = rhs
    {
        lhs = v
    }
}

That solves the compiler error, but it still doesn't work as I'd expect:

// furnace.name is an Optional<String>
furnace.name ??= String?.some("Cooker")
furnace.name                              // "Cooker"
furnace.name ??= String?.none
furnace.name                              // nil

This is also true if you call a function that returns String?.none.

It seems like we could work around that by overloading the operator in an extension for Optional:

extension
Optional
{
    static
    func
    ??=(lhs: inout Self, rhs: Self)
    {
        if let v = rhs
        {
            lhs = v
        }
    }
}

Does that work for you?

Refining a little on what @tem suggested. Although not sure if this is what you're looking for and it's definitely not the most elegant solution:

infix operator ??=: AssignmentPrecedence

func ??=<T>(lhs: inout Optional<T>, rhs: Optional<T>) {
    if lhs == nil {
        lhs = rhs
    } else if let rhs = rhs {
        lhs = rhs
    }
}

furnace?.name ??= "Blaster"
furnace?.name               // Blaster
furnace?.name ??= "Burner"
furnace?.name               // Burner
furnace?.name ??= nil
furnace?.name               // Burner
2 Likes

To consolidate the ideas above, you need two overloads.

public extension Optional {
  static func ??= (optional0: inout Self, optional1: Self) {
    if let some = optional1 {
      optional0 = some
    }
  }

  static func ??= (wrapped: inout Wrapped, optional: Self) {
    if let some = optional {
      wrapped = some
    }
  }
}
3 Likes

I did end up making the extension on Optional, and it does work. I suppose the generic function is better, as it relieves the user of the need to conform other types.

What's the purpose of the Wrapped version? Is that in lieu of any other definitions?

Assigning to non-optionals.

Wow, three different methods here...

  • protocol
  • generic function
  • extending Optional

The last one looks best.

PS. I'd choose slightly nicer and shorter ?=

1 Like

Cool. I do find @Jessy's version easier to understand, and it makes sense to group everything in an extension on Optional, and it removes the need for angle brackets.

Neat idea by the way, I'm going to try to work this into my own projects.

But I think =? would be a better name because it reads more like "assign maybe", similar to calling an optional closure self.action?(), and ?= is more easily confused with ? = which already has a different meaning when the LHS is an optional.

2 Likes

I chose ??= because it was similar to += and the like. Perhaps ?= is better, but the double-question mark was reminiscent of the nil-coalescing operator used in the expanded expression.

1 Like

It does feel like x ??= y should be x = x ?? y though, not the other way around.

8 Likes

Oh, right. Then I think either ??= or =?? or =? would be good, but I'd still avoid ?= because of the potential for confusion with ? = (also, what if you were the type of person to not put spaces around assignment operators, then what?).

=?? would not be as consistent with the compound assignment operators like += but given that this operator works a bit differently (as @Lantua mentions above), it wouldn't benefit much from that consistency anyway in my opinion.

Extra scientific rationale

=?? also has a shorter Hamming distance / Levenshtein distance compared to ??=

What does ? = stand for? That's not valid swift or is it?

I see. you mean:

var x: Int? = xxx
x ?= xxx

I think that spelling is best too, because it's a more clear shorthand for the underlying switch statement.

static func =? (optional0: inout Self, optional1: Self) {
  switch optional1 {
  case let some?:
    optional0 = some
  case .none:
    break 
  }
}
2 Likes

This thread is intriguing to me because it hasn't come up that ??= is ambiguous. Is it

someValue = updatedValue ?? someValue

or

someValue = someValue ?? fallbackValue

What's interesting is the OP defines it the unconventional way around. This matters a lot because ?? isn't commutative. So it would be like defining x /= y as x = y / x. The use case presented makes sense, but if I encountered this operator in some code, it's not the use I would assume at first.

(I realize the goal of the post was not to debate this but to figure out how to define it... just a drive-by observation)

8 Likes

A hypothetical new syntax if there were no limits whatsoever:

someValue = updatedValue ?? _
someValue = _ ?? fallbackValue

Here _ (bike-shedding) is used as "lhs placeholder".

The =? spelling is growing on me, although I’m not sure it's enough to eclipse the ??= code I've already written.

Given the level of interest in this discussion, I wonder how widely used this type of assignment is (i.e. “only change this value if a new value was specified”). I had a handful of places in my code where I was doing this and it was a fun exercise, but not really needed.

I was wondering if I could make it even more compact (and possibly more useful), and I started pondering something like this:

let somethingChanged = conditionallyAssign(destination, source, \.$prop1, \.$prop2, \.$prop3)

I don’t know enough about key paths to know if something like that is even possible across different types. But the idea is, update the properties of destination with the values of the properties of source, and return true iff at least one value changed. This would let me avoid the DB write that happens after the update, in my application.

I suppose ?= could do one, and =? could do the other. :stuck_out_tongue_closed_eyes:

As to ?= being potentially confused with ? =, can it? Won’t the lexer treat them differently? I suppose it wouldn't be too hard to test it, but I haven’t.