Protocol conformance and `Optional`s

Why is this ok:

class Parent {
  var someVar: SomeType? {
    get { ... }
  }

  func someFunc() -> SomeType? {
    ...
  }
}

class Child: Parent {
  // no compiler error
  var someVar: SomeType {
    get { ... }
  }

  // no compiler error
  func someFunc() -> SomeType {
    ...
  }
}


...but this is not ok?

protocol Proto {
  var someVar: SomeType? { get }
}

struct SomeStruct: Proto {
  let someVar: SomeType
}

/// error: Type 'SomeStruct' does not conform to protocol 'Proto'
/// note: Candidate has non-matching type 'SomeType'

I asked a similar question, here is the answer: How does this rule work in regard of ambiguity?

Because SomeStruct does not provide someVar that is an optional type. Your classes Parent and Child do not conform to any protocols, so you are dealing with traditional inheritance and overloading. SomeStruct does not inherit anything, it only has to conform to a protocol, which in your case, it does not, hence the error message.

Your Child class actually has two overloaded computed variables, and two overloaded functions. Overloads are permitted that only differ in the return type.

The answer isn‘t that simple actually, it‘s not just a strict misalignment of protocol requirements here, because theoretically T is a subtype of T? and the protocol of the original post should be satisfied because it‘s safe to promote a non-optional value as an optional one.

Someone has to tackle the issue with a formal proposal still.

3 Likes

@DevAndArtist that's very interesting! And this "trick" works even today, five years later.

@jonprescott yes but the protocol property is a get-only one, so I expect a non-optional implementation to work just fine! Because it seems that a similar rule is already in place for initialisers (look at DevAndArtist's link).

1 Like

I would reply to both you and @DevAndArtist that, although you may be right, theoretically, in my experience, the compiler does not work that way. That may be a limitation of the implementation that needs to be fixed, but, at least in my experience, that is the way the compiler often works.

1 Like

Well sure, right now it does not work because T and T? are strictly speaking different types but with some compiler magic and generalization of the theoretical subtype relationship we could potentially allow this behavior in Swift. (I can‘t wait for it myself.)

Especially because we recently got this paragraph merged into the the manifesto:

Kudos to @anandabits.

I honestly thought so too until I run into a problem, and this is the example from my thread where bar1 and bar2 are the same thing:

My response to @technicated is how the compiler works today. As you've noted, as the generics manifesto is implemented over time, this relationship may be made more robust in the future. As far I can see, it does not work that way now.

1 Like

I think your issue is with the last invocation. I think you might be seeing the effects of nil coalescence. bar2(value::optionalString) becomes in some way, bar2(value: (String?)?) if Value == String?. The nils get coalesced into a single nil, then the pattern matching is done, if I understand the algorithms.

The type information looks like this:

bar1(value: string as String) // Value == String
bar2(value: string as String?) // Value == String
bar1(value: optionalString as String?) // Value == String?
bar2(value: optionalString as String?) // Value == String

One thing to note is your examples aren't quite equal. Your Class property is computed. Your struct property is not. This also isn't allowed in a class when your property has storage, (though still not allowed if your struct property is computed).

class Parent {
    var someVar: String? = "hi"
}

class Child: Parent {
    override var someVar: String = "hi" 
    // ERROR: Cannot override mutable property 'someVar' of type 'String?' with covariant type 'String'
}

It appears this is only allowed when there's no actual storage of the property.

Since a protocol definition can't know whether the adhering property does or does not involve storage, it has to assume it can, which apparently isn't allowed.

In my testing a child class can override an optional computed property with a non-optional version:

class Parent {
    var someVar: String? {"hi"}
}

class Child: Parent {
    override var someVar: String {"hiChild" }
}

let optionalVar: String? = Child().someVar
print(optionalVar) // prints "hiChild"

This is kind of weird behavior and almost feels like a bug. My naive-purely speculative guess is the cause lies in the difference in how computed properties and stored properties are dispatched. But that's delving into a topic I don't know well enough to really comment on :slight_smile:

The problem in your example is not the fact that the property has storage, but the fact that it is both gettable and settable. So a subtype cannot override it with a covariant type because of the setter, which cannot accept a more specific type. If it would, and I use Child cast as a Parent, I could assign nil to a non-optional someVar, and this is not valid.

But the protocol only wants the property to be gettable, so a non optional conformance should be fine... but I guess this is consistent with the following:

protocol Protocol {
  var v: Parent { get }
}

struct MyStruct: Proto {
  var v: Child
}

// `MyStruct` does not conform to `Proto`, if I wanted to do this I should have used
// an associatedtype as the type of `v`

let properties aren't overridable

class Parent {
    let someVar: String? = "hi"
}

class Child: Parent {
    override let someVar: String = "hiChild" 
    // Error: cannot override with a stored property 'someVar'
}

So there's no way to have storage, have only get, and be able to override.

No idea if that's a formal "rule" or just odd emergent behavior. Either way it's not exactly intuitive :man_shrugging:

I don’t know where you get that I want to do this, I never said that.

Moreover your example was using var and my reply was based on that fact, so I don’t even understand this:

My point is that

does not have storage, while

does.

That seems to be why the former is allowed, while the latter is not.

The latter is allowed, but only if you are using an associatedtype, and this is why my original code does not work. This has nothing to do with storage vs get-set.

In my original post, I expected Optional covariance to kick in to let me fulfil the protocol requirement, but that's simply not how the compiler works today - maybe in future it will.

DevAndArtist's "hack" (creating a default implementation) instead works, because in the concrete / implementor type you are overloading the default implementation and this the compiler will allow!

??? Your OP is asking why it's not allowed. I'm saying it seems to have something to do with storage.

Right. This is different behavior from overriding the property in your subclass. This results in 2 separate properties, one with Type? one with with Type.

protocol Proto {
  var someVar: String? { get }
}

extension Proto {
    var someVar: String? { "Extension" }
}
//
struct SomeStruct: Proto {
    var someVar: String { "Implementor" }
}

let thing: String = SomeStruct().someVar
print(thing) // prints "Implementor"

let optionalThing: String? = SomeStruct().someVar
print(optionalThing) // prints "Optional(Extension)"

But I'm saying that this should have nothing to do with storage, but with covariance...

Yeah, in my OP I was asking why, and my understanding now is that even if T is considered a subtype of Optional<T> (although a "special" kind of subtype), this rule is not considered while evaluating protocol conformances. If you want to use covariance in you protocols implementations, you should use an associatedtype. And this brings me to what I meant by "the latter is allowed":

protocol Proto {
  associatedtype T: Parent
  var value: T { get }
}

class Parent {}
class Child: Parent {}

struct Implementor: Proto {
  // This is good, I can use a subtype of `Parent` to fulfil the protocol requirement
  let value: Child
}

The "problem" with this approach - for my specific problem - is that we cannot express the subtype relationship of T and Optional<T> in the type system because that is automagically handled by the compiler as of today.

Yeah, that seems to describe the difference better than my attempt :slight_smile:

FWIW, I prefer to avoid associatedtype unless absolutely necessary.

protocol Proto {
    var someVar: String? { get }
}

struct SomeStruct: Proto {
    var someVar: String? { realVar }
    let realVar: String
}

This is admittedly a rabbit trail, but I'm curious if my way of thinking makes sense.

"subtype" doesn't seem like quite the right abstraction for reasoning about this. That's not the way it's expressed at the Type level since Optional<T> wraps T. My understanding is that generally subtype refers to a parent->child relationship where the child has a superset of the parent's attributes. e.g. all Ducks are Birds, but not all Birds are Ducks. That doesn't describe the relationship between Optional<T> and T.

Optional<T> has all of, or none of T's attributes, and T never has all of Optional<T>'s attributes (since it can never be Optional<T>.none). There is a lot of functionality to easily switch between the two so it often behaves similar to a subtype, but the traditional type->subtype abstraction, (especially from OOP), breaks down for Optional once you get into corner cases like the one in the OP.

It's less of a parent->child relationship, and more of an aunt/uncle->niece/nephew relationship. Maybe there's a name for that? I'm not familiar enough with Type theory to know :innocent:

Terms of Service

Privacy Policy

Cookie Policy