Conditional protocol conformance for Optional where Wrapped - issues with "Any"

I have encountered a weird behaviour, not sure if I am misusing this feature, or is it not fully working as intended.

I want to add general ExpressibleByLiteral conformances to Optionals. Let's take ExpressibleByIntegerLiteral as an example:

extension Optional: ExpressibleByIntegerLiteral where Wrapped: ExpressibleByIntegerLiteral {

    public typealias IntegerLiteralType = Wrapped.IntegerLiteralType

    public init(integerLiteral value: IntegerLiteralType) {
        self = .some(Wrapped.init(integerLiteral: value))
    }
}

The reason for that is that I am using an enum in my mocking library, like following, :

// Simplified for the sake of example
enum Parameter<Value> {
    case any
    case value(Value)
}

extension Parameter: ExpressibleByIntegerLiteral where Value: ExpressibleByIntegerLiteral {
  // ... conformance etc.
}

extension Parameter: ExpressibleByNilLiteral where Value: ExpressibleByNilLiteral {
  // ... conformance etc.
}

The conformance above (for my Parameter enum) is the reason I need Optional do declare it as well.

And it works very well, and I can do something like:

func foo(_ p: Parameter<Int?>) { /* ... */ }

foo(.any)
foo(.value(nil))
foo(.value(1))
foo(1)
foo(2)
foo(nil)
...

But it has one very weird side effect, whenever I create/use type like Any?:

// This does not work:
let bar1: Any? = 1
// Error: Value of protocol type 'Any' cannot conform to 'ExpressibleByIntegerLiteral'; only struct/enum/class types can conform to protocols

// This actually works
let bar2: Any? = 1 as Any 

// This works as well
let bar3: Any? = .init(1)

My question is, why does it behave like that?

I assume that I broke some "magic" that allowed to initialise Optionals using literals, event if they did not declared conformance to ExpressibleBySomethingLiteral (happens for all other literals except nil as well).

Shouldn't Optional utilise this conditional conformance by default?

The "magic" you seem to have broken is a result of two things:

  1. Swift allows implicit conversions from Wrapped to Optional<Wrapped>. This is why you can write something like:
let x: Int = 3
let xOpt: Int? = x

despite the fact that Int and Int? are different types.

  1. ExpressibleBy*Literal protocols generally have a "default" type that is used for literal expressions when the type cannot otherwise be inferred. This is what allows you to write:
let x = 3
print(type(of: x)) // 'Int'

So, without your extension, the resolution of the statement let bar1: Any? = 1 goes something like:

  • Check if Any? conforms to ExpressibleByIntegerLiteral (no).
  • Check if Any conforms to ExpressibleByIntegerLiteral (no).
  • Default the integer literal to Int.
  • Convert the initializer expression to Any.
  • Initialize bar1 using the result of the initializer.

The important thing to note here is that the type checker generally only uses the default literal types as a last resort—if there's other type information available to resolve the expression, that will be taken as the programmer's "true intent," and the default literal type will be ignored. With your conditional conformance of Optional to ExpressibleByIntegerLiteral, the resolution proceeds something like:

  • Check if Any? conforms to ExpressibleByIntegerLiteral.
    • It does, if Wrapped: ExpressibleByIntegerLiteral.
    • Great, add a requirement that Any: ExpressibleByIntegerLiteral.
  • Error, since Any does not conform to ExpressibleByIntegerLiteral.

OK, thanks. Now I get why it happens. I am still unsure though if I should treat it as flaw/inconsistence in Swift, or just have to live with it working like that.

The question that remains is:

Shouldn't Optional declare that conformance explicitly, instead of using these "last resort" literal types? The intent behind conform Optional to ExpressibleBy*Literal when Wrapped conforms to it is kind of clear. It kind of works like that at them moment, but is very implicit. Are there any compelling reasons not to do it like that, explicitly stating conformance for Optional?

Well, the default values for literals are what allow expressions like let bar1: Any? = 1 or let x = 3 to compile in the first place, so whether or not we have the conformances you suggest, we would need those defaults to fall back on.

So with the default literal types present, why not just add the conformances you mention, and then fall back to the default type when the type checker fails on expressions like let bar1: Any? = 1? It might be reasonable to do something like that, but it's not as straightforward as it might seem.

The question is, what should the compiler do when you have a situation where:

  1. The programmer appears to have declared a clear intent (e.g., because there is a conformance Optional: ExpresssibleByIntegerLiteral where Wrapped: ExpressibleByIntegerLiteral)
  2. The programmer's intent leads to a program that cannot compile (e.g., because Any does not conform to ExpressibleByIntegerLiteral)
  3. The default value from a literal protocol would provide a valid solution.

?

In general, it's better to fail to compile than to emit a program with unexpected behavior.

For an exaggerated example, consider the following:

extension Optional: ExpressibleByIntegerLiteral 
    where Wrapped: ExpressibleByIntegerLiteral { ... }

struct Any2: ExpressibleByIntegerLiteral { ... }

let x: Any? = 1

if let y = x as? Any2 {
    ...
} else {
    deleteHardDiskContents("Something went horribly wrong, start over from scratch")
}

Here, the "real" error is a typo—the programmer meant to write Any2 instead of Any in the declaration of x. But if the compilation succeeds, we'll do something very bad!

Now, I don't have the historical context to say whether this specific failure is considered problematic enough to want to prevent programmers from shooting themselves in the foot. I'm just trying to illustrate why it could conceivably be undesirable to have things "just work" when behavior is sufficiently implicit.

Terms of Service

Privacy Policy

Cookie Policy