Search code examples
swiftoption-typetype-inferenceswift-protocolsswift-optionals

How to create a generic function in Swift that will reject the given parameter unless it is an Optional?


This question is a follow up to my earlier question: I expected the system to report non protocol conformance, but it does not! Why?

Please read the referred question for you to get a better idea of the constraints at hand.

I created a generic function in Swift that will reject its parameter unless such a parameter is Optional. The function I created fully works and does what I desire.

Meaning, any calls to onlyCallableByAnOptable(...), even inside an if let, will yield error due to non-protocol conformance, exactly as desired.

Errors like: Argument type 'UIColor' does not conform to expected type 'Optable'

My only question is: Is there a simpler solution?

To make it clear: func onlyCallableWithAnOptinalParameter<T>(:T)->T needs to work when called in an if let statement like func test() does.

protocol Optable {
    associatedtype OptableType
    func optionalOptable() -> OptableType?
}

func onlyCallableByAnOptable<T>( _ value: T) -> T.OptableType? where T: Optable {
    return value.optionalOptable()
}


extension Optional: Optable {
    typealias OptableType = Wrapped //: Wrapped is the type of the element, as defined in Optional
    func optionalOptable() -> OptableType? {
        return self
    }
}


class TestOptable {
    static func test()
    {
        let c = UIColor.blue
        let s = "hi"
        let i = Int(10)
        let oi: Int? = 10

        if let v = onlyCallableByAnOptable(c) {  // ERROR, as was desired.
            print("color \(v)") 
        }
        if let v = onlyCallableByAnOptable(s) {  // ERROR, as was desired.
            print("string \(v)") 
        }
        if let v = onlyCallableByAnOptable(i) {  // ERROR, as was desired.
            print("integer \(v)") 
        }
        if let v = onlyCallableByAnOptable(oi) {  // OK, as expected.
            print("optional integer \(v)") 
        }
    }
}

Solution

  • You might want to give this protocol a better name, but I don't foresee any problems with it as-is, unless you're making your own ExpressibleByNilLiteral types that don't wrap.

    protocol ExpressibleByNilLiteral: Swift.ExpressibleByNilLiteral {
      associatedtype Wrapped
    }
    
    extension Optional: ExpressibleByNilLiteral { }
    
    func onlyCallableByAnOptional<Optional: ExpressibleByNilLiteral>(_ optional: Optional) -> Optional.Wrapped? {
      optional as? Optional.Wrapped
    }
    

    Recommendation: use an initializer. (Downside is the argument label being necessary to disambiguate, but I personally like the explicitness because of how weird this case is. i.e. Swift makes it easy to enforce that something is not optional, but not vice versa.)

    extension Optional: ExpressibleByNilLiteral {
      init<Optional: ExpressibleByNilLiteral>(optional: Optional) where Optional.Wrapped == Wrapped {
        self = optional as? Wrapped
      }
    }
    

    +

    if let v = Optional(optional: i) {  // ERROR, as was desired.
      print("integer \(v)")
    }
    if let v = Optional(optional: oi) {  // OK, as expected.
      print("optional integer \(v)")
    }