Protocol conformance and `Optional`s

@Joe_Groff do we have a better name for this kind of relationship? I always thought of this (and Array, etc) as subtypes...

Note to me and to everyone else

I want to make clear that using the "trick" does not work, in the following sense:

struct V {
  let fixedValue: Int = 10
}

protocol P {
  var v: V? { get }
}

extension  P {
  var v: V? { nil }
}

struct S1: P {
  // note, this *is not* optional
  let v: V = V()
}

struct S2: P {
  // note, this *is* optional
  let v: V? = V()
}

The conformance of S1 to P is only valid through the default implementation. The non-optional overload is never seen from the protocol, so if a function on an extension of P makes use of v in the context of S1, nil and not V() will be used!

On the other hand S2 conforms to the protocol deciding not to use the default implementation and declaring a property v of type V? itself. Calling a function of P in the context of S2 will indeed use .some(V()).

extension  P {
  func printV() {
      print(v)
  }
}

S1().fn()
// nil
S2().fn()
// Optional(V(fixedValue: 10))

I asked a similar question here: Why covariance does not apply when working with protocols in some situations

I believe the answer is that in the current compiler implementation, when dealing with protocols, Liskov substitution principle is not fully supported in some circumstances. Some deeper reasons are also explained in that thread by other guys.

1 Like

I would still call these subtype relationships. To me, it seems like a bug that protocol conformances don't allow for variance between the implementation's and requirement's types; hopefully that'll be something we fix one day.

8 Likes

That would be great! In your opinion would this also apply to the OP situation? Using a non-optional to fit a Protocol's requirement that's Optional.

With typical generic Types (e.g. Array<T>, Set<T>) the abstraction makes sense to me. I'm unsure how to reconcile it with the complication of Optional<T> being an enum. In many cases it behaves similar to a typical parent-child relationship, but the semantics seem meaningfully different.

Maybe my thinking is overly-influenced by OOP and class-subclass relationships. Does "subtype" have a more generalized definition here? :thinking:

IIRC (aka, don't quote me on this :stuck_out_tongue_closed_eyes:) subtyping relationship only requires substitutability, which applies nicely to T? and T. Only that class inheritance usually support subtyping relationship out of the box.

1 Like

Donā€˜t think about that particular subtype relationship as strict as it could be. Otherwise Never could never become the bottom type. These are just language specific details. Similarly ImplicitUnwrappedOptional is also an Optional.

Yes you can substitute Optional<T>.some with T, but you can't substitute Optional<T>.none with T. It also doesn't adhere to ExpressableByNilLiteral (nor should it), so the relationship is more complicated.

Isnā€˜t the substitution relation the other way around? You can kinda upcast T to T! and finally to T?.

There is also an 'enum subtyping' where a subtype enum of a given parent enum would not add new cases but rather remove cases. Thatā€˜s exactly what we have here, we removed the none case and are left with some(T) which in fact is just T.

Btw. here is a great (non-official) document of different related ideas from @anandabits: ValueSubtypeManifesto.md Ā· GitHub

Agh...Never might be the worst thing to search for :joy:

At least with typical OOP subtypes, If T? is a subtype of T then you should be able to use T? wherever T is used. The current relationship is the opposite. Unless you're saying enum subtyping (something I was unaware of) is basically the oposite?

1 Like

Yeah in terms of value subtyping (not regular objects) for enum subtypes you remove cases which guarantees that a super enum will have all cases of any of its subtype. In our case the upcast from T to T? would just magically wrap a non-optional value into the Optional.some(t).

It is important to observe that the semantics of value subtype relationships are much different than object oriented inheritance relationships. They have nothing to do with dynamic dispatch or implementation sharing. Another important difference is that providing a subclass instance where a superclass instance is required never requires a change in representation of the object reference itself.

Ah. I think this is what I was looking for! So, a Value subtype is a subset of it's parent, while an Object subtype is a superset of it's parent. Seems like we're in need of a new term :thinking:

1 Like

About that, you should rather search for 'bottom type'.

But it was never said that T? was a subtype of T. It was stated the other way around: T is a subtype of T? And you can in fact use a T anywhere a T? is needed.

1 Like

It really depends on your definition. e.g. if it was a typical class-subclass relationship, when T is a subtype of T? you would be able to do T.instance?.doAThing() since all the functionality of T? should be available to T.

It's also not 100% replaceable anyway:

func doAThing<T: ExpressibleByNilLiteral>(theThing: T) {
    print("Did A Thing")
}

let otherThing: String? = "hi"
let realThing: String = "hi"

doAThing(theThing: otherThing)
doAThing(theThing: realThing) // error: argument type 'String' does not conform to expected type 'ExpressibleByNilLiteral'
// this would work if String was a subclass of String?

I might be inventing things here but... maybe subclass and subtype are different kinds of relationships, where subclass is a "stronger" kind of relationship than subtype.

A subtype does not have all the functionalities of its "supertype", but can be automatically converted / wrapped into it to acquire these functionalities. A subclass instead always directly has all the functionalities of its "supertype" (i.e. its superclass).

The conclusion I came to from that is available earlier in the thread. I'm unsure how official or industry-defined it is to distinguish class-subtypes and value-subtypes, but the distinction makes sense in my brain :slight_smile: