简体   繁体   中英

How to implement a infix custom operator that handles optionality in swift

I'm trying to implement a custom operator for Collections similar to the Elvis operator (?: in kotlin, ?? in swift), but in addition to checking nullability, the operator also checks if the collection is Empty.

However, when I try to use the operator, the code doesn't compile. The compiler throws an "ambiguous use of operator" error.

The implementation of the?? operator in the swift language seems to be really similar to mine, so I'm a little lost.. Any help to understand why this happens, and how to fix it, will be greatly appreciated.


/// Returns the first argument if it's not null and not empty, otherwise will return the second argument.
infix operator ?/ : NilCoalescingPrecedence

@inlinable func ?/ <T: Collection>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T {
    if let value = optional,
       !value.isEmpty {
        return value
    } else {
        return try defaultValue()
    }
}

@inlinable func ?/ <T: Collection>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T? {
    if let value = optional,
       !value.isEmpty {
        return value
    } else {
        return try defaultValue()
    }
}

func test() {
    let optionalValue: String? = nil
    
    let value1: String = optionalValue ?? "default value" // This works
    let value2: String = optionalValue ?/ "default value" // This works
    let value3: String? = optionalValue ?/ nil // This works
    let value4: String? = optionalValue ?? nil // This works
    let value5: String? = optionalValue ?? "default value" // This works
    let value6: String? = optionalValue ?/ "default value" // This dont compile: Ambiguous use of operator '?/'
}

The standard implementation for the?? operator can be found at: https://github.com/apple/swift/blob/main/stdlib/public/core/Optional.swift , just search for "?? <" in the browser.

Maybe I'm using the wrong approach to solve this problem. If anyone knows a better solution will be great too.

Short answer, you can't do that. Probably??, provided by swift, works because swift has it's own powers and it treats these situations on it's own. But for our code, it doesn't work like that.

What will happen there is:

For the expression: let value2: String = optionalValue?/ "default value" .

  • First the compiler will look to the optionalValue parameter and will find 2 methods that accept an optional as first parameter;
  • Then it will look to the second parameter ( defaultValue ) that is a closure returning a non-optional T: Collection instance, and it will filter and match the first operator overload;
  • The last thing is the return value that is a non-optional T: Collection instance, and the first method is complient;
  • Success;

For the expression: let value4: String? = optionalValue?/ "default value" let value4: String? = optionalValue?/ "default value" .

  • First the compiler will look to the optionalValue parameter and will find 2 methods that accept an optional as first parameter;
  • Then it will look to the second parameter ( defaultValue ) that is a closure returning an optional T: Collection aka Optional<T> where T: Collection instance, and then it will find 2 options for the 2 parameters so far;
  • At this point it will look for the return type of the function, but it will fail too;
  • Compiler error;

The reason that it fails is that T: Collection in your code is translated to String . And for the defaultValue return type an non-optional String fits the first and second methods, leading the compiler to be unsure which one it should use.

let string: String? = "value" let string: String? = "value" and let string: String? = Optional<String>.init("value") let string: String? = Optional<String>.init("value") are the same thing for the compiler due to implicit conversions that Swift does.

So there is not way to make this work from where my knowledge stands right now.

It turns out that swift has an attribute called @_disfavoredOverload, when I use it on the second method, everything works as intended.

Now the implementation of the second method is:

@_disfavoredOverload
@inlinable func ?/ <T: Collection>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T? {
    if let value = optional,
       !value.isEmpty {
        return value
    } else {
        return try defaultValue()
    }
}

Discovered this on the swift forum: https://forums.swift.org/t/how-to-implement-a-infix-custom-operator-that-handles-optionality-in-swift/47260/3

Based on the implementation, optionalValue?/ "default value" will never return nil .

So you can do one of these:

Don't use optional if you know it will be not optional: (RECOMENDED)

let value6: String = optionalValue ?/ "default value"

Make it optional:

let value6: String? = (optionalValue ?/ "default value")!

Make it use the second function:

let anotherOptionalValue: String? = "default value"
let value6: String? = (optionalValue ?/ anotherOptionalValue

Use @_disfavoredOverload on the second function: (PROHABITED)

@_disfavoredOverload
@inlinable func ?/ <T: Collection>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T? { ... }

⚠️ Attributes marked with _ is not designed for general use!


Note:

Try using type inference . So your code will be more optimized and the compiler will have fewer issues to worry about.

let value6 = optionalValue ?/ "default value"
let value7 = optionalValue ?/ anotherOptionalValue

Without _disfavoredOverload, your original value6 needed a.some(...) on the RHS to disambiguate.

As you said "Maybe I'm using the wrong approach to solve this problem", I would suggest a different kind of solution. Instead of:

if let value = optional,
   !value.isEmpty {
    return value
} else {
    return try defaultValue()
}

Let's write this:

value.ifNotEmpty ?? defaultValue()

Extend collection like so:

extension Collection {
    var ifNotEmpty: Self? {
        isEmpty ? nil : self
    }
}

extension Optional where Wrapped: Collection {
    var isEmpty: Bool {
        map(\.isEmpty) ?? true
    }

    var ifNotEmpty: Self {
        isEmpty ? nil : self
    }
}

Now we don't need the custom operator, and that means the call sites can read like optionalValue.ifNotEmpty?? "default value" optionalValue.ifNotEmpty?? "default value" :

let optionalValue: String? = ""
let value1: String = optionalValue.ifNotEmpty ?? "default value"
let value3: String? = optionalValue.ifNotEmpty
let value5: String? = optionalValue.ifNotEmpty ?? "default value"

Depending on your preference, maybe this reads more clearly than the?/ operator

But you also get to play nicely with unwrapping:

if let foo = optionalValue.ifNotEmpty {
    ...
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM