Making protocol conformance inheritance controllable


(Joe Groff) #1

I've had a number of twitter conversations with users who have valiantly fought our type system and lost when trying to make their protocols interact well with non-final classes. A few from recent memory:

- Rob Napier struggling to implement a `copy` method that works well with subclasses: https://twitter.com/cocoaphony/status/660914612850843648
and making the observation that the interaction of protocol conformance and subtyping is very difficult to teach.

- Matt Bischoff trying to make Cocoa class clusters retroactively conform to his factory protocol:
https://twitter.com/anandabits/status/664294382774849536

and Karl Adam trying to do the same:
https://gist.github.com/thekarladam/c3094769cc8c87bf55e3

These problems stem from the way protocol conformances currently interact with class inheritance—specifically, that if a class conforms to a protocol, then all of its possible derived classes also conform. This seems like the obvious way things should be, but in practice it ends up fighting how many classes are intended to be used. Often only a base class is intended to be the public interface, and derived classes are only implementation details—Cocoa class clusters are a great example of this. The inheritance of protocol conformances also imposes a bunch of knock-on complexity on conforming classes—initializer requirements must be satisfied by `required` initializers (which then must be overridden in all derived classes, to the pain of anyone touching an NSCoding-inherited class), and methods often must return dynamic `Self` when they'd really prefer to return the base class.

To mitigate these issues, I'd like to float the idea that protocol conformances *not be* inherited by default. If you declare a class as conforming to a protocol, only exactly that class can be bound to a type parameter constrained by that protocol:

protocol Runcible {}
class A: Runcible { }
class B { }

func foo<T: Runcible>(x: T) {}

foo(B()) // calls foo with T == A

Since subclasses are still subtypes of the base class, in many cases client code won't have to change at all, since derived instances can implicitly upconvert to their conforming base class when used in protocol types or generics that only the base class conforms to. (There are cases like if the type parameter appears in a NonCovariant<T> type where this isn't possible, though.) Protocol requirements for a non-inherited conformance don't need to be `required` initializers, or maintain covariant returns:

protocol Fungible {
  init()
  static func funged() -> Self
}

class C: Fungible {
  init() {} // Non-required init is fine, since subclasses aren't directly Fungible

  // Non-Self return is fine too
  class func funged() -> C { return C() }
}

An individual subclass that wanted to refine the conformance could do so by `override`-ing it, and providing any necessary covariant overrides of initializers and methods:

class D: C, override Fungible {
  // D must provide its own init()
  init() { super.init() }

  // D must override funged() to return D instead of C
  override class func funged() -> D { return D() }
}

And if a class hierarchy really wants to impose a conformance on all possible subclasses, as happens today, we could let you opt in to that:

class E: required Fungible {
  // init() must be required of all subclasses
  required init() { }

  // funged() must return a covariant object
  class func funged() -> Self { return Self() }
}

This is undoubtedly a complication of the language, but I think it might let us more accurately model a lot of things people seem to want to do in practice with class hierarchies and protocols, and it simplifies the behavior of the arguably common case where inheritance of the conformance isn't desired. What do you all think?

-Joe


Allow non-inherited conformances
Un-requiring required initializers
Un-requiring required initializers
(Matthew Johnson) #2

I have run into this issue myself. It is definitely a problem and we need a solution for it. It is exacerbated by the fact that many Cocoa types which would naturally be value types or final classes in Swift are not designed this way due to their Objective-C heritage. If Cocoa was designed in Swift and with an emphasis on composition rather than inheritance many cases of the problem would vanish and I’m not sure whether we would have a problem or not, but unfortunately we do.

My initial reaction to this solution is that it is probably a good approach. One change I would consider is making it mirror method overriding a bit more closely. Specifically, we might want to also make it possible to specify “final” protocol conformance as well as add the ability to “require" method overrides. Aligning the capabilities would leave one less nuance for everyone to remember and reduce the complexity added by the proposal.

One alternative that might be worth exploring is whether we can introduce a concept supporting class clusters more directly as well as a FINAL annotation for Objective-C classes which really shouldn’t be inherited or where subclassing is of dubious value (inheriting to work around a bug is dubious IMO despite the practical value some see in it). A combination of these two approaches might solve the problem for all or most of the classes we really care about. I’m not suggesting this is a better approach, just sharing it for consideration.

Matthew

···

On Dec 10, 2015, at 8:04 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

I've had a number of twitter conversations with users who have valiantly fought our type system and lost when trying to make their protocols interact well with non-final classes. A few from recent memory:

- Rob Napier struggling to implement a `copy` method that works well with subclasses: https://twitter.com/cocoaphony/status/660914612850843648
and making the observation that the interaction of protocol conformance and subtyping is very difficult to teach.

- Matt Bischoff trying to make Cocoa class clusters retroactively conform to his factory protocol:
https://twitter.com/anandabits/status/664294382774849536

and Karl Adam trying to do the same:
https://gist.github.com/thekarladam/c3094769cc8c87bf55e3

These problems stem from the way protocol conformances currently interact with class inheritance—specifically, that if a class conforms to a protocol, then all of its possible derived classes also conform. This seems like the obvious way things should be, but in practice it ends up fighting how many classes are intended to be used. Often only a base class is intended to be the public interface, and derived classes are only implementation details—Cocoa class clusters are a great example of this. The inheritance of protocol conformances also imposes a bunch of knock-on complexity on conforming classes—initializer requirements must be satisfied by `required` initializers (which then must be overridden in all derived classes, to the pain of anyone touching an NSCoding-inherited class), and methods often must return dynamic `Self` when they'd really prefer to return the base class.

To mitigate these issues, I'd like to float the idea that protocol conformances *not be* inherited by default. If you declare a class as conforming to a protocol, only exactly that class can be bound to a type parameter constrained by that protocol:

protocol Runcible {}
class A: Runcible { }
class B { }

func foo<T: Runcible>(x: T) {}

foo(B()) // calls foo with T == A

Since subclasses are still subtypes of the base class, in many cases client code won't have to change at all, since derived instances can implicitly upconvert to their conforming base class when used in protocol types or generics that only the base class conforms to. (There are cases like if the type parameter appears in a NonCovariant<T> type where this isn't possible, though.) Protocol requirements for a non-inherited conformance don't need to be `required` initializers, or maintain covariant returns:

protocol Fungible {
  init()
  static func funged() -> Self
}

class C: Fungible {
  init() {} // Non-required init is fine, since subclasses aren't directly Fungible

  // Non-Self return is fine too
  class func funged() -> C { return C() }
}

An individual subclass that wanted to refine the conformance could do so by `override`-ing it, and providing any necessary covariant overrides of initializers and methods:

class D: C, override Fungible {
  // D must provide its own init()
  init() { super.init() }

  // D must override funged() to return D instead of C
  override class func funged() -> D { return D() }
}

And if a class hierarchy really wants to impose a conformance on all possible subclasses, as happens today, we could let you opt in to that:

class E: required Fungible {
  // init() must be required of all subclasses
  required init() { }

  // funged() must return a covariant object
  class func funged() -> Self { return Self() }
}

This is undoubtedly a complication of the language, but I think it might let us more accurately model a lot of things people seem to want to do in practice with class hierarchies and protocols, and it simplifies the behavior of the arguably common case where inheritance of the conformance isn't desired. What do you all think?

-Joe

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Chris Lattner) #3

Very interesting approach, I think I like it. That said, this is subtle enough that we’d probably have to prototype it to see what falls out in practice on existing real world code.

-Chris

···

On Dec 10, 2015, at 6:04 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

To mitigate these issues, I'd like to float the idea that protocol conformances *not be* inherited by default. If you declare a class as conforming to a protocol, only exactly that class can be bound to a type parameter constrained by that protocol:

protocol Runcible {}
class A: Runcible { }
class B : A { }

func foo<T: Runcible>(x: T) {}

foo(B()) // calls foo with T == A

Since subclasses are still subtypes of the base class, in many cases client code won't have to change at all, since derived instances can implicitly upconvert to their conforming base class when used in protocol types or generics that only the base class conforms to.


(Michel Fortin) #4

I think the concept makes sense. But you're changing the default behaviour here. Is this because you expect protocols should not be inherited most of the time? That most protocols in Cocoa shouldn't be inherited? That goes against "obvious way things should be".

Also, shouldn't this be allowed too?

  class F: C, required override Fungible {
    required init() { }
    class func funged() -> Self { return Self() }
  }

Personally, I'd tend to make 'required' the default mode, and use "static Fungible" for the odd one that does not apply to subtypes. And I'd get rid of override before the protocol name, just use "static Fungible" again in the subclass:

  class C: static Fungible {
    init() {}
    class func funged() -> C { return C() }
  }
  class D: C, static Fungible {
    init() {}
    override class func funged() -> D { return D() }
  }
  class E: Fungible {
    required init() {}
    class func funged() -> Self { return Self() }
  }
  class F: C, Fungible {
    required init() {}
    override class func funged() -> Self { return Self() }
  }

But in the end the syntax depends on what default behaviour is desired.

···

Le 10 déc. 2015 à 21:04, Joe Groff via swift-evolution <swift-evolution@swift.org> a écrit :

And if a class hierarchy really wants to impose a conformance on all possible subclasses, as happens today, we could let you opt in to that:

class E: required Fungible {
  // init() must be required of all subclasses
  required init() { }

  // funged() must return a covariant object
  class func funged() -> Self { return Self() }
}

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(thorsten@portableinnovations.de) #5

Very interesting! Looks very promising, but I will have to think it through more closely and throw some protoypes at it to get a better grasp on the idea.

-Thorsten

···

Am 11.12.2015 um 03:04 schrieb Joe Groff via swift-evolution <swift-evolution@swift.org>:

I've had a number of twitter conversations with users who have valiantly fought our type system and lost when trying to make their protocols interact well with non-final classes. A few from recent memory:

- Rob Napier struggling to implement a `copy` method that works well with subclasses: https://twitter.com/cocoaphony/status/660914612850843648
and making the observation that the interaction of protocol conformance and subtyping is very difficult to teach.

- Matt Bischoff trying to make Cocoa class clusters retroactively conform to his factory protocol:
https://twitter.com/anandabits/status/664294382774849536

and Karl Adam trying to do the same:
https://gist.github.com/thekarladam/c3094769cc8c87bf55e3

These problems stem from the way protocol conformances currently interact with class inheritance—specifically, that if a class conforms to a protocol, then all of its possible derived classes also conform. This seems like the obvious way things should be, but in practice it ends up fighting how many classes are intended to be used. Often only a base class is intended to be the public interface, and derived classes are only implementation details—Cocoa class clusters are a great example of this. The inheritance of protocol conformances also imposes a bunch of knock-on complexity on conforming classes—initializer requirements must be satisfied by `required` initializers (which then must be overridden in all derived classes, to the pain of anyone touching an NSCoding-inherited class), and methods often must return dynamic `Self` when they'd really prefer to return the base class.

To mitigate these issues, I'd like to float the idea that protocol conformances *not be* inherited by default. If you declare a class as conforming to a protocol, only exactly that class can be bound to a type parameter constrained by that protocol:

protocol Runcible {}
class A: Runcible { }
class B { }

func foo<T: Runcible>(x: T) {}

foo(B()) // calls foo with T == A

Since subclasses are still subtypes of the base class, in many cases client code won't have to change at all, since derived instances can implicitly upconvert to their conforming base class when used in protocol types or generics that only the base class conforms to. (There are cases like if the type parameter appears in a NonCovariant<T> type where this isn't possible, though.) Protocol requirements for a non-inherited conformance don't need to be `required` initializers, or maintain covariant returns:

protocol Fungible {
  init()
  static func funged() -> Self
}

class C: Fungible {
  init() {} // Non-required init is fine, since subclasses aren't directly Fungible

  // Non-Self return is fine too
  class func funged() -> C { return C() }
}

An individual subclass that wanted to refine the conformance could do so by `override`-ing it, and providing any necessary covariant overrides of initializers and methods:

class D: C, override Fungible {
  // D must provide its own init()
  init() { super.init() }

  // D must override funged() to return D instead of C
  override class func funged() -> D { return D() }
}

And if a class hierarchy really wants to impose a conformance on all possible subclasses, as happens today, we could let you opt in to that:

class E: required Fungible {
  // init() must be required of all subclasses
  required init() { }

  // funged() must return a covariant object
  class func funged() -> Self { return Self() }
}

This is undoubtedly a complication of the language, but I think it might let us more accurately model a lot of things people seem to want to do in practice with class hierarchies and protocols, and it simplifies the behavior of the arguably common case where inheritance of the conformance isn't desired. What do you all think?

-Joe

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(plx) #6

The problem addressed is real but I have a few reservations about the proposal as-sketched.

My first reservation is that, as-described, this would seem very likely to have some very bizarre and unintuitive cross-interactions if implemented alongside any construct along these lines:

// extension conforming `Foo` to `Bar` whenever $Condition is met
extension Foo: Bar where $Condition [

}

…due to ambiguities wherein, say, a base class explicitly conforms to some protocols, and consequently obtains an additional conformance via some conditional extension; a subclass explicitly re-conforms to the same protocols the base explicitly conforms-to…should it pick up that same “additional” conformance or not?

My second reservation is that, as I understand the proposal, it seems you could easily create protocol constraints for which no useful type would “naturally” work; e.g. as I understand it, if I happened to have this:

class Whatever<
  K:SomeBaseClass
  where
  K:ProtocolA,
  K:ProtocolB,
  K:ProtocolC>

…then to be able to actually write this:

let whatever = Whatever<SomeClass>()

…we would need `SomeClass` to inherit from `SomeBaseClass` and also *explicitly* re-declare its conformance to each of `ProtocolA`, `ProtocolB`, and `ProtocolC`, correct?

If so, IMHO, this would be a change for the worse in the context of UI-level code; it’s quite possible there'd be no specific class that could be used as `$SomeClass` (without a lot of busywork re-declarations of conformance); moreover, you’d have to do it again for each concrete type you expected to use with `Whatever`, which seems like a rather non-generic form of generics. But I might be misunderstanding.

Finally, as an observation, there are at least two different “flavors” of classes:

- value-ish classes, which mostly exist to represent some value (lots of Foundation, many “model classes”, etc.)
- identity-ish classes, which are specifically used as “entities” (UIView, CALayer, NSURLSession, NSOperation, etc.)

…and whereas the `value-ish` classes are often hitting awkward issues due to how protocol conformance and class inheritance interact, the identity-ish classes *largely* aren’t (with the main exception being due to inheriting `NSCoder` and perhaps `NSCopying`, which although awkward don’t seem awkward enough on their own to merit such a sweeping language-level change; a smaller change could address these).

In the identity-ish scenario I think subclasses inheriting their parent's protocol inheritance is *exactly* the usual — and useful — case, and thus I’d hope any solution to the issues encountered by value-ish classes would avoid making the creation-and-use of identity-ish classes too much more difficult / more tedious; please keep in mind that the “common” case in reports-of-problems may not be the “common” case in the field; when things work as-expected you may not hear as much about them, because there’s not much to say.

···

On Dec 10, 2015, at 8:04 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

I've had a number of twitter conversations with users who have valiantly fought our type system and lost when trying to make their protocols interact well with non-final classes. A few from recent memory:

- Rob Napier struggling to implement a `copy` method that works well with subclasses: https://twitter.com/cocoaphony/status/660914612850843648
and making the observation that the interaction of protocol conformance and subtyping is very difficult to teach.

- Matt Bischoff trying to make Cocoa class clusters retroactively conform to his factory protocol:
https://twitter.com/anandabits/status/664294382774849536

and Karl Adam trying to do the same:
https://gist.github.com/thekarladam/c3094769cc8c87bf55e3

These problems stem from the way protocol conformances currently interact with class inheritance—specifically, that if a class conforms to a protocol, then all of its possible derived classes also conform. This seems like the obvious way things should be, but in practice it ends up fighting how many classes are intended to be used. Often only a base class is intended to be the public interface, and derived classes are only implementation details—Cocoa class clusters are a great example of this. The inheritance of protocol conformances also imposes a bunch of knock-on complexity on conforming classes—initializer requirements must be satisfied by `required` initializers (which then must be overridden in all derived classes, to the pain of anyone touching an NSCoding-inherited class), and methods often must return dynamic `Self` when they'd really prefer to return the base class.

To mitigate these issues, I'd like to float the idea that protocol conformances *not be* inherited by default. If you declare a class as conforming to a protocol, only exactly that class can be bound to a type parameter constrained by that protocol:

protocol Runcible {}
class A: Runcible { }
class B { }

func foo<T: Runcible>(x: T) {}

foo(B()) // calls foo with T == A

Since subclasses are still subtypes of the base class, in many cases client code won't have to change at all, since derived instances can implicitly upconvert to their conforming base class when used in protocol types or generics that only the base class conforms to. (There are cases like if the type parameter appears in a NonCovariant<T> type where this isn't possible, though.) Protocol requirements for a non-inherited conformance don't need to be `required` initializers, or maintain covariant returns:

protocol Fungible {
  init()
  static func funged() -> Self
}

class C: Fungible {
  init() {} // Non-required init is fine, since subclasses aren't directly Fungible

  // Non-Self return is fine too
  class func funged() -> C { return C() }
}

An individual subclass that wanted to refine the conformance could do so by `override`-ing it, and providing any necessary covariant overrides of initializers and methods:

class D: C, override Fungible {
  // D must provide its own init()
  init() { super.init() }

  // D must override funged() to return D instead of C
  override class func funged() -> D { return D() }
}

And if a class hierarchy really wants to impose a conformance on all possible subclasses, as happens today, we could let you opt in to that:

class E: required Fungible {
  // init() must be required of all subclasses
  required init() { }

  // funged() must return a covariant object
  class func funged() -> Self { return Self() }
}

This is undoubtedly a complication of the language, but I think it might let us more accurately model a lot of things people seem to want to do in practice with class hierarchies and protocols, and it simplifies the behavior of the arguably common case where inheritance of the conformance isn't desired. What do you all think?

-Joe

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(TJ Usiyan) #7

I think that this is a good idea overall. I agree that `required override`
might necessary as well.

···

---
TJ

On Fri, Dec 11, 2015 at 7:34 AM, Joe Groff via swift-evolution < swift-evolution@swift.org> wrote:

I've had a number of twitter conversations with users who have valiantly
fought our type system and lost when trying to make their protocols
interact well with non-final classes. A few from recent memory:

- Rob Napier struggling to implement a `copy` method that works well with
subclasses: https://twitter.com/cocoaphony/status/660914612850843648
and making the observation that the interaction of protocol conformance
and subtyping is very difficult to teach.

- Matt Bischoff trying to make Cocoa class clusters retroactively conform
to his factory protocol:
https://twitter.com/anandabits/status/664294382774849536

and Karl Adam trying to do the same:
https://gist.github.com/thekarladam/c3094769cc8c87bf55e3

These problems stem from the way protocol conformances currently interact
with class inheritance—specifically, that if a class conforms to a
protocol, then all of its possible derived classes also conform. This seems
like the obvious way things should be, but in practice it ends up fighting
how many classes are intended to be used. Often only a base class is
intended to be the public interface, and derived classes are only
implementation details—Cocoa class clusters are a great example of this.
The inheritance of protocol conformances also imposes a bunch of knock-on
complexity on conforming classes—initializer requirements must be satisfied
by `required` initializers (which then must be overridden in all derived
classes, to the pain of anyone touching an NSCoding-inherited class), and
methods often must return dynamic `Self` when they'd really prefer to
return the base class.

To mitigate these issues, I'd like to float the idea that protocol
conformances *not be* inherited by default. If you declare a class as
conforming to a protocol, only exactly that class can be bound to a type
parameter constrained by that protocol:

protocol Runcible {}

class A: Runcible { }
class B { }

func foo<T: Runcible>(x: T) {}

foo(B()) // calls foo with T == A

Since subclasses are still subtypes of the base class, in many cases
client code won't have to change at all, since derived instances can
implicitly upconvert to their conforming base class when used in protocol
types or generics that only the base class conforms to. (There are cases
like if the type parameter appears in a NonCovariant<T> type where this
isn't possible, though.) Protocol requirements for a non-inherited
conformance don't need to be `required` initializers, or maintain covariant
returns:

protocol Fungible {
  init()
  static func funged() -> Self
}

class C: Fungible {
  init() {} // Non-required init is fine, since subclasses aren't directly
Fungible

  // Non-Self return is fine too
  class func funged() -> C { return C() }
}

An individual subclass that wanted to refine the conformance could do so
by `override`-ing it, and providing any necessary covariant overrides of
initializers and methods:

class D: C, override Fungible {
  // D must provide its own init()
  init() { super.init() }

  // D must override funged() to return D instead of C
  override class func funged() -> D { return D() }
}

And if a class hierarchy really wants to impose a conformance on all
possible subclasses, as happens today, we could let you opt in to that:

class E: required Fungible {
  // init() must be required of all subclasses
  required init() { }

  // funged() must return a covariant object
  class func funged() -> Self { return Self() }
}

This is undoubtedly a complication of the language, but I think it might
let us more accurately model a lot of things people seem to want to do in
practice with class hierarchies and protocols, and it simplifies the
behavior of the arguably common case where inheritance of the conformance
isn't desired. What do you all think?

-Joe

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Nate Cook) #8

If for no other reason*, I would support this idea because it should allow us to make NSURL conform to StringLiteralConvertible. This was possible back when literal convertibility was handled via static methods, but the `required` initializer requirement currently prevents conforming via an extension.

Nate

*your other reasons are all good too

···

On Dec 10, 2015, at 8:04 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

I've had a number of twitter conversations with users who have valiantly fought our type system and lost when trying to make their protocols interact well with non-final classes. A few from recent memory:

- Rob Napier struggling to implement a `copy` method that works well with subclasses: https://twitter.com/cocoaphony/status/660914612850843648
and making the observation that the interaction of protocol conformance and subtyping is very difficult to teach.

- Matt Bischoff trying to make Cocoa class clusters retroactively conform to his factory protocol:
https://twitter.com/anandabits/status/664294382774849536

and Karl Adam trying to do the same:
https://gist.github.com/thekarladam/c3094769cc8c87bf55e3

These problems stem from the way protocol conformances currently interact with class inheritance—specifically, that if a class conforms to a protocol, then all of its possible derived classes also conform. This seems like the obvious way things should be, but in practice it ends up fighting how many classes are intended to be used. Often only a base class is intended to be the public interface, and derived classes are only implementation details—Cocoa class clusters are a great example of this. The inheritance of protocol conformances also imposes a bunch of knock-on complexity on conforming classes—initializer requirements must be satisfied by `required` initializers (which then must be overridden in all derived classes, to the pain of anyone touching an NSCoding-inherited class), and methods often must return dynamic `Self` when they'd really prefer to return the base class.

To mitigate these issues, I'd like to float the idea that protocol conformances *not be* inherited by default. If you declare a class as conforming to a protocol, only exactly that class can be bound to a type parameter constrained by that protocol:

protocol Runcible {}
class A: Runcible { }
class B { }

func foo<T: Runcible>(x: T) {}

foo(B()) // calls foo with T == A

Since subclasses are still subtypes of the base class, in many cases client code won't have to change at all, since derived instances can implicitly upconvert to their conforming base class when used in protocol types or generics that only the base class conforms to. (There are cases like if the type parameter appears in a NonCovariant<T> type where this isn't possible, though.) Protocol requirements for a non-inherited conformance don't need to be `required` initializers, or maintain covariant returns:

protocol Fungible {
  init()
  static func funged() -> Self
}

class C: Fungible {
  init() {} // Non-required init is fine, since subclasses aren't directly Fungible

  // Non-Self return is fine too
  class func funged() -> C { return C() }
}

An individual subclass that wanted to refine the conformance could do so by `override`-ing it, and providing any necessary covariant overrides of initializers and methods:

class D: C, override Fungible {
  // D must provide its own init()
  init() { super.init() }

  // D must override funged() to return D instead of C
  override class func funged() -> D { return D() }
}

And if a class hierarchy really wants to impose a conformance on all possible subclasses, as happens today, we could let you opt in to that:

class E: required Fungible {
  // init() must be required of all subclasses
  required init() { }

  // funged() must return a covariant object
  class func funged() -> Self { return Self() }
}

This is undoubtedly a complication of the language, but I think it might let us more accurately model a lot of things people seem to want to do in practice with class hierarchies and protocols, and it simplifies the behavior of the arguably common case where inheritance of the conformance isn't desired. What do you all think?

-Joe

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Jordan Rose) #9

At first this didn't make any sense to me. Now it makes some kind of twisted sense, and I'm working my way through the details:

protocol Proto { /*…*/ }
class Base : Proto { /*…*/ }
class Subclass : Base {}

- Protocols with no initializer requirements and no use of 'Self' are trivially safe here, because there's no way to recover the original type.

- Methods returning 'Self' will silently return the base type. This is often reasonable, but it's still a little weird. There's also no way to have a protocol that requires dynamic-self behavior for a factory method.

Subclass.createViaProtocol() // produces a Base

- Initializers are not guaranteed to be inherited. That's not weird for Swift. It's a little weird for Objective-C, but we already emit placeholders for non-inherited initializers to produce a runtime failure, which is what most NSCoder-bemoaners want anyway.

- Methods taking 'Self' will silently accept the base type. If you override a conformance, you have to continue accepting the base type for these requirements, but that's just normal override rules.

- Overriding a conformance still doesn't let you change the associated types, even covariantly, because extensions to the base type may have used them in new positions. (Alternately, associated types and generic parameters are still invariant and we'd need a language feature to make them non-invariant.)

- Generic constraints will have to be taught to infer the base type.

foo(Subclass()) // calls foo<Base>(Subclass())
let obj: Subclass = bar() // error, because bar<Base>() will return a Base

- What does "subclass is Proto" do? What does "Subclass.self is Proto.Type" do? What does "Subclasses.instancesConformToProtocol(Proto.self)" do? My guess is 'true' for all three even though it's not exactly correct for Objective-C protocols with initializer requirements, but then what's the query that returns 'false'?

- Given all this, what benefit does required conformance bring? Just shorthand for making all the initializers required? (Which may be important if you got some of them through protocol extensions.)

Gosh. Maybe we can do it.

Jordan

···

On Dec 10, 2015, at 18:04, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

I've had a number of twitter conversations with users who have valiantly fought our type system and lost when trying to make their protocols interact well with non-final classes. A few from recent memory:

- Rob Napier struggling to implement a `copy` method that works well with subclasses: https://twitter.com/cocoaphony/status/660914612850843648
and making the observation that the interaction of protocol conformance and subtyping is very difficult to teach.

- Matt Bischoff trying to make Cocoa class clusters retroactively conform to his factory protocol:
https://twitter.com/anandabits/status/664294382774849536

and Karl Adam trying to do the same:
https://gist.github.com/thekarladam/c3094769cc8c87bf55e3

These problems stem from the way protocol conformances currently interact with class inheritance—specifically, that if a class conforms to a protocol, then all of its possible derived classes also conform. This seems like the obvious way things should be, but in practice it ends up fighting how many classes are intended to be used. Often only a base class is intended to be the public interface, and derived classes are only implementation details—Cocoa class clusters are a great example of this. The inheritance of protocol conformances also imposes a bunch of knock-on complexity on conforming classes—initializer requirements must be satisfied by `required` initializers (which then must be overridden in all derived classes, to the pain of anyone touching an NSCoding-inherited class), and methods often must return dynamic `Self` when they'd really prefer to return the base class.

To mitigate these issues, I'd like to float the idea that protocol conformances *not be* inherited by default. If you declare a class as conforming to a protocol, only exactly that class can be bound to a type parameter constrained by that protocol:

protocol Runcible {}
class A: Runcible { }
class B { }

func foo<T: Runcible>(x: T) {}

foo(B()) // calls foo with T == A

Since subclasses are still subtypes of the base class, in many cases client code won't have to change at all, since derived instances can implicitly upconvert to their conforming base class when used in protocol types or generics that only the base class conforms to. (There are cases like if the type parameter appears in a NonCovariant<T> type where this isn't possible, though.) Protocol requirements for a non-inherited conformance don't need to be `required` initializers, or maintain covariant returns:

protocol Fungible {
  init()
  static func funged() -> Self
}

class C: Fungible {
  init() {} // Non-required init is fine, since subclasses aren't directly Fungible

  // Non-Self return is fine too
  class func funged() -> C { return C() }
}

An individual subclass that wanted to refine the conformance could do so by `override`-ing it, and providing any necessary covariant overrides of initializers and methods:

class D: C, override Fungible {
  // D must provide its own init()
  init() { super.init() }

  // D must override funged() to return D instead of C
  override class func funged() -> D { return D() }
}

And if a class hierarchy really wants to impose a conformance on all possible subclasses, as happens today, we could let you opt in to that:

class E: required Fungible {
  // init() must be required of all subclasses
  required init() { }

  // funged() must return a covariant object
  class func funged() -> Self { return Self() }
}

This is undoubtedly a complication of the language, but I think it might let us more accurately model a lot of things people seem to want to do in practice with class hierarchies and protocols, and it simplifies the behavior of the arguably common case where inheritance of the conformance isn't desired. What do you all think?

-Joe

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


#10

Been bitten also.

I have run into this issue myself. It is definitely a problem and we need a solution for it. It is exacerbated by the fact that many Cocoa types which would naturally be value types or final classes in Swift are not designed this way due to their Objective-C heritage.

Foundation also exposes many non-final classes such as the immutable/mutable couples NSArray/NSMutableArray etc.

initializer requirements must be satisfied by `required` initializers (which then must be overridden in all derived classes, to the pain of anyone touching an NSCoding-inherited class), and methods often must return dynamic `Self` when they'd really prefer to return the base class.

This is especially painful when you want to extend existing non-final classes, such as Foundation classes.

***Fortunately***, those often provide the needed constructors (including copy constructors), which alleviates the fact that one can not add a required initializer to an extension to a non-final class.

    protocol P {
        static func f() -> Self?
    }

    extension NSString : P {
        class func f() -> Self? {
            return self.init(string: "foo")
        }
    }
    
    NSString.f()
    NSMutableString.f()

But the presence of those constructors in Foundation looks like a matter of luck.

Gwendal Roué

···

Le 11 déc. 2015 à 04:03, Matthew Johnson via swift-evolution <swift-evolution@swift.org> a écrit :
Le 11 déc. 2015 à 03:04, Joe Groff via swift-evolution <swift-evolution@swift.org> a écrit :


(Joe Groff) #11

And if a class hierarchy really wants to impose a conformance on all possible subclasses, as happens today, we could let you opt in to that:

class E: required Fungible {
// init() must be required of all subclasses
required init() { }

// funged() must return a covariant object
class func funged() -> Self { return Self() }
}

I think the concept makes sense. But you're changing the default behaviour here. Is this because you expect protocols should not be inherited most of the time? That most protocols in Cocoa shouldn't be inherited? That goes against "obvious way things should be".

It's a good question which behavior ought to be default. The good news is that the distinction is only observable for protocols with covariant requirements—initializers, methods returning Self, or properties of type Self. A more conservative change might be:

- preserve the current inheritance behavior for protocols without covariant requirements;
- when a class conforms to a protocol with covariant requirements, it must specify whether the conformance is `static` or `required`, so that neither behavior is default.

Also, shouldn't this be allowed too?

  class F: C, required override Fungible {
    required init() { }
    class func funged() -> Self { return Self() }
  }

'required override' would be unnecessary in any case. The base class requiring the conformance implies that every derived class inherits it already, and derived classes can already influence the conformance by normal method overriding.

-Joe

···

On Dec 10, 2015, at 8:04 PM, Michel Fortin <michel.fortin@michelf.ca> wrote:
Le 10 déc. 2015 à 21:04, Joe Groff via swift-evolution <swift-evolution@swift.org> a écrit :

Personally, I'd tend to make 'required' the default mode, and use "static Fungible" for the odd one that does not apply to subtypes. And I'd get rid of override before the protocol name, just use "static Fungible" again in the subclass:

  class C: static Fungible {
    init() {}
    class func funged() -> C { return C() }
  }
  class D: C, static Fungible {
    init() {}
    override class func funged() -> D { return D() }
  }
  class E: Fungible {
    required init() {}
    class func funged() -> Self { return Self() }
  }
  class F: C, Fungible {
    required init() {}
    override class func funged() -> Self { return Self() }
  }

But in the end the syntax depends on what default behaviour is desired.

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(Greg Parker) #12

I don't like it.

The problems in the motivating examples all center around protocols with factory methods or other uses of Self. Changing the behavior of all protocols seems like the wrong tool to solve that problem. There must be a narrower solution that improves those cases without changing so much existing code and understanding. (Are there any other languages with similar behavior?)

The "NSCoding is annoying" example is particularly dangerous. Historically the alternatives have been:

* Don't require subclasses to implement init(coder:). This leads to difficult bugs where a subclass does not implement init(coder:) but somebody tries to encode and decode an instance of it. There are no compile-time errors, no runtime errors, and the resulting decoded object is corrupt.

* Require subclasses to implement init(coder:), even if all it does is abort(). This provides good runtime diagnostics against the bug above, at the cost of some subclass boilerplate. This is what Swift does, and what ObjC now does with the new designated initializer enforcement.

I fear that non-inherited protocols will reintroduce the old bugs. The subclasses will not be required to implement init(coder:), and encoding and decoding an instance of such a class will do corrupt things at runtime with no runtime diagnostics.

···

On Dec 10, 2015, at 18:04, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

I've had a number of twitter conversations with users who have valiantly fought our type system and lost when trying to make their protocols interact well with non-final classes. A few from recent memory:

- Rob Napier struggling to implement a `copy` method that works well with subclasses: https://twitter.com/cocoaphony/status/660914612850843648
and making the observation that the interaction of protocol conformance and subtyping is very difficult to teach.

- Matt Bischoff trying to make Cocoa class clusters retroactively conform to his factory protocol:
https://twitter.com/anandabits/status/664294382774849536

and Karl Adam trying to do the same:
https://gist.github.com/thekarladam/c3094769cc8c87bf55e3

These problems stem from the way protocol conformances currently interact with class inheritance—specifically, that if a class conforms to a protocol, then all of its possible derived classes also conform. This seems like the obvious way things should be, but in practice it ends up fighting how many classes are intended to be used. Often only a base class is intended to be the public interface, and derived classes are only implementation details—Cocoa class clusters are a great example of this. The inheritance of protocol conformances also imposes a bunch of knock-on complexity on conforming classes—initializer requirements must be satisfied by `required` initializers (which then must be overridden in all derived classes, to the pain of anyone touching an NSCoding-inherited class), and methods often must return dynamic `Self` when they'd really prefer to return the base class.

To mitigate these issues, I'd like to float the idea that protocol conformances *not be* inherited by default.

--
Greg Parker gparker@apple.com Runtime Wrangler


(plx) #13

I still don’t understand what this proposal is actually proposing in cases like this:

class Root {}
class ImplementerOfA : Root, ProtocolA {}
class ImplementerOfB : ImplementerOfA, ProtocolB {}
class Base : ImplementerOfB {}

func exampleFunction<
  T:Base
  where
  T:ProtocolA,
  T:ProtocolB>(target: T) -> Whatever

…if I have this:

class CustomizedBase : Base {}

…what is supposed to happen if I try to write this:

let whatever = exampleFunction(CustomizedBase())

...?

If I understand the logic in the proposal, it seems like I *can’t* call `exampleFunction` here with `CustomizedBase`:

- it can’t be `CustomizedBase` b/c it doesn’t (re)implement `ProtocolA` or `ProtocolB`
- it can’t be `Base` b/c it doesn’t (re)implement `ProtocolA` or `ProtocolB`

…and even if we change it to this:

func exampleFunction<
  T:Root
  where
  T:ProtocolA,
  T:ProtocolB>(target: T) -> Whatever

…then we still can’t call it with `CustomizedBase`, since:

- it can’t be `CustomizedBase` b/c it doesn’t (re)implement `ProtocolA` or `ProtocolB`
- it can’t be `Base` b/c it doesn’t (re)implement `ProtocolA` or `ProtocolB`
- it can’t be `ImplementerOfB` b/c it doesn’t (re)implement `ProtocolA`
- it can’t be `ImplementerOfA` b/c it doesn’t implement `ProtocolB`
- it can’t be `Root` b/c it doesn’t implement `ProtocolA` or `ProtocolB`

…so we can’t make this call, no?

Am I missing something obvious in either example? If so, which type would get matched (in either example)?

···

On Dec 14, 2015, at 8:08 PM, Jordan Rose via swift-evolution <swift-evolution@swift.org> wrote:

At first this didn't make any sense to me. Now it makes some kind of twisted sense, and I'm working my way through the details:

protocol Proto { /*…*/ }
class Base : Proto { /*…*/ }
class Subclass : Base {}

- Protocols with no initializer requirements and no use of 'Self' are trivially safe here, because there's no way to recover the original type.

- Methods returning 'Self' will silently return the base type. This is often reasonable, but it's still a little weird. There's also no way to have a protocol that requires dynamic-self behavior for a factory method.

Subclass.createViaProtocol() // produces a Base

- Initializers are not guaranteed to be inherited. That's not weird for Swift. It's a little weird for Objective-C, but we already emit placeholders for non-inherited initializers to produce a runtime failure, which is what most NSCoder-bemoaners want anyway.

- Methods taking 'Self' will silently accept the base type. If you override a conformance, you have to continue accepting the base type for these requirements, but that's just normal override rules.

- Overriding a conformance still doesn't let you change the associated types, even covariantly, because extensions to the base type may have used them in new positions. (Alternately, associated types and generic parameters are still invariant and we'd need a language feature to make them non-invariant.)

- Generic constraints will have to be taught to infer the base type.

foo(Subclass()) // calls foo<Base>(Subclass())
let obj: Subclass = bar() // error, because bar<Base>() will return a Base

- What does "subclass is Proto" do? What does "Subclass.self is Proto.Type" do? What does "Subclasses.instancesConformToProtocol(Proto.self)" do? My guess is 'true' for all three even though it's not exactly correct for Objective-C protocols with initializer requirements, but then what's the query that returns 'false'?

- Given all this, what benefit does required conformance bring? Just shorthand for making all the initializers required? (Which may be important if you got some of them through protocol extensions.)

Gosh. Maybe we can do it.

Jordan

On Dec 10, 2015, at 18:04, Joe Groff via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I've had a number of twitter conversations with users who have valiantly fought our type system and lost when trying to make their protocols interact well with non-final classes. A few from recent memory:

- Rob Napier struggling to implement a `copy` method that works well with subclasses: https://twitter.com/cocoaphony/status/660914612850843648
and making the observation that the interaction of protocol conformance and subtyping is very difficult to teach.

- Matt Bischoff trying to make Cocoa class clusters retroactively conform to his factory protocol:
https://twitter.com/anandabits/status/664294382774849536

and Karl Adam trying to do the same:
https://gist.github.com/thekarladam/c3094769cc8c87bf55e3

These problems stem from the way protocol conformances currently interact with class inheritance—specifically, that if a class conforms to a protocol, then all of its possible derived classes also conform. This seems like the obvious way things should be, but in practice it ends up fighting how many classes are intended to be used. Often only a base class is intended to be the public interface, and derived classes are only implementation details—Cocoa class clusters are a great example of this. The inheritance of protocol conformances also imposes a bunch of knock-on complexity on conforming classes—initializer requirements must be satisfied by `required` initializers (which then must be overridden in all derived classes, to the pain of anyone touching an NSCoding-inherited class), and methods often must return dynamic `Self` when they'd really prefer to return the base class.

To mitigate these issues, I'd like to float the idea that protocol conformances *not be* inherited by default. If you declare a class as conforming to a protocol, only exactly that class can be bound to a type parameter constrained by that protocol:

protocol Runcible {}
class A: Runcible { }
class B { }

func foo<T: Runcible>(x: T) {}

foo(B()) // calls foo with T == A

Since subclasses are still subtypes of the base class, in many cases client code won't have to change at all, since derived instances can implicitly upconvert to their conforming base class when used in protocol types or generics that only the base class conforms to. (There are cases like if the type parameter appears in a NonCovariant<T> type where this isn't possible, though.) Protocol requirements for a non-inherited conformance don't need to be `required` initializers, or maintain covariant returns:

protocol Fungible {
  init()
  static func funged() -> Self
}

class C: Fungible {
  init() {} // Non-required init is fine, since subclasses aren't directly Fungible

  // Non-Self return is fine too
  class func funged() -> C { return C() }
}

An individual subclass that wanted to refine the conformance could do so by `override`-ing it, and providing any necessary covariant overrides of initializers and methods:

class D: C, override Fungible {
  // D must provide its own init()
  init() { super.init() }

  // D must override funged() to return D instead of C
  override class func funged() -> D { return D() }
}

And if a class hierarchy really wants to impose a conformance on all possible subclasses, as happens today, we could let you opt in to that:

class E: required Fungible {
  // init() must be required of all subclasses
  required init() { }

  // funged() must return a covariant object
  class func funged() -> Self { return Self() }
}

This is undoubtedly a complication of the language, but I think it might let us more accurately model a lot of things people seem to want to do in practice with class hierarchies and protocols, and it simplifies the behavior of the arguably common case where inheritance of the conformance isn't desired. What do you all think?

-Joe

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Thorsten Seitz) #14

I've had a number of twitter conversations with users who have valiantly fought our type system and lost when trying to make their protocols interact well with non-final classes. A few from recent memory:

- Rob Napier struggling to implement a `copy` method that works well with subclasses: https://twitter.com/cocoaphony/status/660914612850843648
and making the observation that the interaction of protocol conformance and subtyping is very difficult to teach.

- Matt Bischoff trying to make Cocoa class clusters retroactively conform to his factory protocol:
https://twitter.com/anandabits/status/664294382774849536

and Karl Adam trying to do the same:
https://gist.github.com/thekarladam/c3094769cc8c87bf55e3

These problems stem from the way protocol conformances currently interact with class inheritance—specifically, that if a class conforms to a protocol, then all of its possible derived classes also conform. This seems like the obvious way things should be, but in practice it ends up fighting how many classes are intended to be used. Often only a base class is intended to be the public interface, and derived classes are only implementation details—Cocoa class clusters are a great example of this. The inheritance of protocol conformances also imposes a bunch of knock-on complexity on conforming classes—initializer requirements must be satisfied by `required` initializers (which then must be overridden in all derived classes, to the pain of anyone touching an NSCoding-inherited class), and methods often must return dynamic `Self` when they'd really prefer to return the base class.

To mitigate these issues, I'd like to float the idea that protocol conformances *not be* inherited by default. If you declare a class as conforming to a protocol, only exactly that class can be bound to a type parameter constrained by that protocol:

What exactly does that mean?

If A conforms to Runcible and B is derived from A that means that B is a subtype of A and can be used everywhere an A can be used (substitution principle).
Therefore if I can use an A where a Runcible is required, I must be able to use a B as well. Subtyping is transitive which gets somehow broken in this proposal (or maybe not, see below).

protocol Runcible {}
class A: Runcible { }
class B : A { }

func foo<T: Runcible>(x: T) {}

foo(B()) // calls foo with T == A

This would just hold for the static type of T, right?
The dynamic type of x still stays B and dynamic dispatch would respect that whereas static dispatch (e.g. for extension methods declared for A and B separately) would use the static type A (i.e. dispatch to the extension method declared for A).

Since subclasses are still subtypes of the base class, in many cases client code won't have to change at all, since derived instances can implicitly upconvert to their conforming base class when used in protocol types or generics that only the base class conforms to. (There are cases like if the type parameter appears in a NonCovariant<T> type where this isn't possible, though.)

Protocol requirements for a non-inherited conformance don't need to be `required` initializers, or maintain covariant returns:

The required initializers are probably the motivation for this change.
Is there a problem with covariant returns as well? I'd think a subclass does not have to do anything to just get them, so I don't see a problem there.

If I understand the desire behind the proposal correctly, it is about
(a) stopping Self types to vary below some point in the hierarchy
(b) controlling the requiredness of initializers within the hierarchy (doesn’t that collide with the idea of requiring an initializer in the first place?)

I have the vague felling (or hope) that this can be done more nicely in other ways without tampering with subtyping.

Joe Groff recently proposed in another thread something similar for Self types in Equatable:

public protocol Equatable {
    typealias EquatesWith = Self where Self: EquatesWith
    func ==(lhs: EquatesWith, rhs: EquatesWith) -> Bool
}

This way subclasses or subprotocols can decide where they want to fix EquatesWith to a fixed value:

protocol Fungible {
    typealias F = Self where Self: F
    static func funged() -> F
}

class C: Fungible {
    class func funged() -> F { return C() }
}

class D: C {
    override class func funged() -> F { return D() }
}

class X: C {
    typealias F = C
}

D.funged() has static type D
X.funged() has static type C

Would this fit your expectations for the Self type issues? Or have I misunderstood the problem completely?

Maybe something similar can be done for requiredness of initializers.

-Thorsten

···

On Dec 10, 2015, at 8:04 PM, Joe Groff via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

protocol Fungible {
  init()
  static func funged() -> Self
}

class C: Fungible {
  init() {} // Non-required init is fine, since subclasses aren't directly Fungible

  // Non-Self return is fine too
  class func funged() -> C { return C() }
}

An individual subclass that wanted to refine the conformance could do so by `override`-ing it, and providing any necessary covariant overrides of initializers and methods:

class D: C, override Fungible {
  // D must provide its own init()
  init() { super.init() }

  // D must override funged() to return D instead of C
  override class func funged() -> D { return D() }
}

And if a class hierarchy really wants to impose a conformance on all possible subclasses, as happens today, we could let you opt in to that:

class E: required Fungible {
  // init() must be required of all subclasses
  required init() { }

  // funged() must return a covariant object
  class func funged() -> Self { return Self() }
}

This is undoubtedly a complication of the language, but I think it might let us more accurately model a lot of things people seem to want to do in practice with class hierarchies and protocols, and it simplifies the behavior of the arguably common case where inheritance of the conformance isn't desired. What do you all think?

-Joe


(Matthew Johnson) #15

It is exacerbated by the fact that many Cocoa types which would naturally be value types or final classes in Swift are not designed this way due to their Objective-C heritage.

Foundation also exposes many non-final classes such as the immutable/mutable couples NSArray/NSMutableArray etc.

These are naturally value types in Swift and when designed that way don't exhibit the problem mentioned in this thread.

Also FWIW, it's not really safe to consider NSArray "immutable" because it is a reference type with a mutable subtype. Specific instances of NSArray can be considered immutable when you know they are not actually instances of NSMutableArray and the interface to NSArray obviously does not allow mutation, but the assumption that NSArray is immutable has been the source of many bugs for many developers. There is a reason it is not named NSImmutableArray. The design of Cocoa could have been a lot more clear about this if it also had an NSImmutableArray class that also inherited from NSArray.


(Michel Fortin) #16

Does it make sense to say the protocol is `required` when it contains no initializers? For instance:

  protocol Something {
    static func give() -> Self?
  }
  class A: required Something {
    class func give() -> Self? { return nil }
  }
  
If I then derive B from A, am I required to override func give()?

  class B: A {
    // missing func 'give' or not?
  }

It would certainly make sense to want the protocol to be `static` in this case, to allow B.give to return a value of type A. But `required` doesn't seem to have any relevant meaning.

···

Le 11 déc. 2015 à 15:21, Joe Groff <jgroff@apple.com> a écrit :

It's a good question which behavior ought to be default. The good news is that the distinction is only observable for protocols with covariant requirements—initializers, methods returning Self, or properties of type Self. A more conservative change might be:

- preserve the current inheritance behavior for protocols without covariant requirements;
- when a class conforms to a protocol with covariant requirements, it must specify whether the conformance is `static` or `required`, so that neither behavior is default.

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(Matthew Johnson) #17

The problems in the motivating examples all center around protocols with factory methods or other uses of Self. Changing the behavior of all protocols seems like the wrong tool to solve that problem. There must be a narrower solution that improves those cases without changing so much existing code and understanding. (Are there any other languages with similar behavior?)

I had a Twitter exchange with Joe on this topic about a month ago as well. It was also an initializer / factory method example where I was unable to write conformance for Foundation classes.

I mentioned the possible solution of allowing a protocol to specify something like Self that would be non-covarying. I don't have any idea what a good name for this might but it would have worked for the problem I was trying to solve. It is certainly a much narrower solution.

Matthew

If I remember correctly he


(Erica Sadun) #18

This is an issue that comes up constantly around me. I'm probably taking the wrong view on things but it seems as if a lot of the problem comes from trying to squeeze Cocoa classes, especially UIKit ones, into a Swift system where native reference types are more often naturally final. There's a kind of turbulence that builds up between the two systems.

-- E

···

On Dec 14, 2015, at 7:53 PM, Greg Parker via swift-evolution <swift-evolution@swift.org> wrote:

On Dec 10, 2015, at 18:04, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

I've had a number of twitter conversations with users who have valiantly fought our type system and lost when trying to make their protocols interact well with non-final classes. A few from recent memory:

- Rob Napier struggling to implement a `copy` method that works well with subclasses: https://twitter.com/cocoaphony/status/660914612850843648
and making the observation that the interaction of protocol conformance and subtyping is very difficult to teach.

- Matt Bischoff trying to make Cocoa class clusters retroactively conform to his factory protocol:
https://twitter.com/anandabits/status/664294382774849536

and Karl Adam trying to do the same:
https://gist.github.com/thekarladam/c3094769cc8c87bf55e3

These problems stem from the way protocol conformances currently interact with class inheritance—specifically, that if a class conforms to a protocol, then all of its possible derived classes also conform. This seems like the obvious way things should be, but in practice it ends up fighting how many classes are intended to be used. Often only a base class is intended to be the public interface, and derived classes are only implementation details—Cocoa class clusters are a great example of this. The inheritance of protocol conformances also imposes a bunch of knock-on complexity on conforming classes—initializer requirements must be satisfied by `required` initializers (which then must be overridden in all derived classes, to the pain of anyone touching an NSCoding-inherited class), and methods often must return dynamic `Self` when they'd really prefer to return the base class.

To mitigate these issues, I'd like to float the idea that protocol conformances *not be* inherited by default.

I don't like it.

The problems in the motivating examples all center around protocols with factory methods or other uses of Self. Changing the behavior of all protocols seems like the wrong tool to solve that problem. There must be a narrower solution that improves those cases without changing so much existing code and understanding. (Are there any other languages with similar behavior?)

The "NSCoding is annoying" example is particularly dangerous. Historically the alternatives have been:

* Don't require subclasses to implement init(coder:). This leads to difficult bugs where a subclass does not implement init(coder:) but somebody tries to encode and decode an instance of it. There are no compile-time errors, no runtime errors, and the resulting decoded object is corrupt.

* Require subclasses to implement init(coder:), even if all it does is abort(). This provides good runtime diagnostics against the bug above, at the cost of some subclass boilerplate. This is what Swift does, and what ObjC now does with the new designated initializer enforcement.

I fear that non-inherited protocols will reintroduce the old bugs. The subclasses will not be required to implement init(coder:), and encoding and decoding an instance of such a class will do corrupt things at runtime with no runtime diagnostics.

--
Greg Parker gparker@apple.com Runtime Wrangler

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Joe Groff) #19

I still don’t understand what this proposal is actually proposing in cases like this:

class Root {}
class ImplementerOfA : Root, ProtocolA {}
class ImplementerOfB : ImplementerOfA, ProtocolB {}
class Base : ImplementerOfB {}

func exampleFunction<
  T:Base
  where
  T:ProtocolA,
  T:ProtocolB>(target: T) -> Whatever

…if I have this:

class CustomizedBase : Base {}

…what is supposed to happen if I try to write this:

let whatever = exampleFunction(CustomizedBase())

...?

If I understand the logic in the proposal, it seems like I *can’t* call `exampleFunction` here with `CustomizedBase`:

- it can’t be `CustomizedBase` b/c it doesn’t (re)implement `ProtocolA` or `ProtocolB`
- it can’t be `Base` b/c it doesn’t (re)implement `ProtocolA` or `ProtocolB`

…and even if we change it to this:

func exampleFunction<
  T:Root
  where
  T:ProtocolA,
  T:ProtocolB>(target: T) -> Whatever

…then we still can’t call it with `CustomizedBase`, since:

- it can’t be `CustomizedBase` b/c it doesn’t (re)implement `ProtocolA` or `ProtocolB`
- it can’t be `Base` b/c it doesn’t (re)implement `ProtocolA` or `ProtocolB`
- it can’t be `ImplementerOfB` b/c it doesn’t (re)implement `ProtocolA`
- it can’t be `ImplementerOfA` b/c it doesn’t implement `ProtocolB`
- it can’t be `Root` b/c it doesn’t implement `ProtocolA` or `ProtocolB`

…so we can’t make this call, no?

Am I missing something obvious in either example? If so, which type would get matched (in either example)?

You are correct that cases like this simply wouldn't work, without ImplementerOfB explicitly refining the conformance of ProtocolA.

-Joe

···

On Dec 14, 2015, at 7:21 PM, plx via swift-evolution <swift-evolution@swift.org> wrote:

On Dec 14, 2015, at 8:08 PM, Jordan Rose via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

At first this didn't make any sense to me. Now it makes some kind of twisted sense, and I'm working my way through the details:

protocol Proto { /*…*/ }
class Base : Proto { /*…*/ }
class Subclass : Base {}

- Protocols with no initializer requirements and no use of 'Self' are trivially safe here, because there's no way to recover the original type.

- Methods returning 'Self' will silently return the base type. This is often reasonable, but it's still a little weird. There's also no way to have a protocol that requires dynamic-self behavior for a factory method.

Subclass.createViaProtocol() // produces a Base

- Initializers are not guaranteed to be inherited. That's not weird for Swift. It's a little weird for Objective-C, but we already emit placeholders for non-inherited initializers to produce a runtime failure, which is what most NSCoder-bemoaners want anyway.

- Methods taking 'Self' will silently accept the base type. If you override a conformance, you have to continue accepting the base type for these requirements, but that's just normal override rules.

- Overriding a conformance still doesn't let you change the associated types, even covariantly, because extensions to the base type may have used them in new positions. (Alternately, associated types and generic parameters are still invariant and we'd need a language feature to make them non-invariant.)

- Generic constraints will have to be taught to infer the base type.

foo(Subclass()) // calls foo<Base>(Subclass())
let obj: Subclass = bar() // error, because bar<Base>() will return a Base

- What does "subclass is Proto" do? What does "Subclass.self is Proto.Type" do? What does "Subclasses.instancesConformToProtocol(Proto.self)" do? My guess is 'true' for all three even though it's not exactly correct for Objective-C protocols with initializer requirements, but then what's the query that returns 'false'?

- Given all this, what benefit does required conformance bring? Just shorthand for making all the initializers required? (Which may be important if you got some of them through protocol extensions.)

Gosh. Maybe we can do it.

Jordan

On Dec 10, 2015, at 18:04, Joe Groff via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I've had a number of twitter conversations with users who have valiantly fought our type system and lost when trying to make their protocols interact well with non-final classes. A few from recent memory:

- Rob Napier struggling to implement a `copy` method that works well with subclasses: https://twitter.com/cocoaphony/status/660914612850843648
and making the observation that the interaction of protocol conformance and subtyping is very difficult to teach.

- Matt Bischoff trying to make Cocoa class clusters retroactively conform to his factory protocol:
https://twitter.com/anandabits/status/664294382774849536

and Karl Adam trying to do the same:
https://gist.github.com/thekarladam/c3094769cc8c87bf55e3

These problems stem from the way protocol conformances currently interact with class inheritance—specifically, that if a class conforms to a protocol, then all of its possible derived classes also conform. This seems like the obvious way things should be, but in practice it ends up fighting how many classes are intended to be used. Often only a base class is intended to be the public interface, and derived classes are only implementation details—Cocoa class clusters are a great example of this. The inheritance of protocol conformances also imposes a bunch of knock-on complexity on conforming classes—initializer requirements must be satisfied by `required` initializers (which then must be overridden in all derived classes, to the pain of anyone touching an NSCoding-inherited class), and methods often must return dynamic `Self` when they'd really prefer to return the base class.

To mitigate these issues, I'd like to float the idea that protocol conformances *not be* inherited by default. If you declare a class as conforming to a protocol, only exactly that class can be bound to a type parameter constrained by that protocol:

protocol Runcible {}
class A: Runcible { }
class B { }

func foo<T: Runcible>(x: T) {}

foo(B()) // calls foo with T == A

Since subclasses are still subtypes of the base class, in many cases client code won't have to change at all, since derived instances can implicitly upconvert to their conforming base class when used in protocol types or generics that only the base class conforms to. (There are cases like if the type parameter appears in a NonCovariant<T> type where this isn't possible, though.) Protocol requirements for a non-inherited conformance don't need to be `required` initializers, or maintain covariant returns:

protocol Fungible {
  init()
  static func funged() -> Self
}

class C: Fungible {
  init() {} // Non-required init is fine, since subclasses aren't directly Fungible

  // Non-Self return is fine too
  class func funged() -> C { return C() }
}

An individual subclass that wanted to refine the conformance could do so by `override`-ing it, and providing any necessary covariant overrides of initializers and methods:

class D: C, override Fungible {
  // D must provide its own init()
  init() { super.init() }

  // D must override funged() to return D instead of C
  override class func funged() -> D { return D() }
}

And if a class hierarchy really wants to impose a conformance on all possible subclasses, as happens today, we could let you opt in to that:

class E: required Fungible {
  // init() must be required of all subclasses
  required init() { }

  // funged() must return a covariant object
  class func funged() -> Self { return Self() }
}

This is undoubtedly a complication of the language, but I think it might let us more accurately model a lot of things people seem to want to do in practice with class hierarchies and protocols, and it simplifies the behavior of the arguably common case where inheritance of the conformance isn't desired. What do you all think?

-Joe

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Joe Groff) #20

I've had a number of twitter conversations with users who have valiantly fought our type system and lost when trying to make their protocols interact well with non-final classes. A few from recent memory:

- Rob Napier struggling to implement a `copy` method that works well with subclasses: https://twitter.com/cocoaphony/status/660914612850843648
and making the observation that the interaction of protocol conformance and subtyping is very difficult to teach.

- Matt Bischoff trying to make Cocoa class clusters retroactively conform to his factory protocol:
https://twitter.com/anandabits/status/664294382774849536

and Karl Adam trying to do the same:
https://gist.github.com/thekarladam/c3094769cc8c87bf55e3

These problems stem from the way protocol conformances currently interact with class inheritance—specifically, that if a class conforms to a protocol, then all of its possible derived classes also conform. This seems like the obvious way things should be, but in practice it ends up fighting how many classes are intended to be used. Often only a base class is intended to be the public interface, and derived classes are only implementation details—Cocoa class clusters are a great example of this. The inheritance of protocol conformances also imposes a bunch of knock-on complexity on conforming classes—initializer requirements must be satisfied by `required` initializers (which then must be overridden in all derived classes, to the pain of anyone touching an NSCoding-inherited class), and methods often must return dynamic `Self` when they'd really prefer to return the base class.

To mitigate these issues, I'd like to float the idea that protocol conformances *not be* inherited by default.

I don't like it.

The problems in the motivating examples all center around protocols with factory methods or other uses of Self. Changing the behavior of all protocols seems like the wrong tool to solve that problem. There must be a narrower solution that improves those cases without changing so much existing code and understanding. (Are there any other languages with similar behavior?)

The change in inheritance behavior could be constrained to only protocols with factory or initializer requirements, since it makes no difference whether the conformance is inherited if there are no covariant uses of Self in the protocol.

The "NSCoding is annoying" example is particularly dangerous. Historically the alternatives have been:

* Don't require subclasses to implement init(coder:). This leads to difficult bugs where a subclass does not implement init(coder:) but somebody tries to encode and decode an instance of it. There are no compile-time errors, no runtime errors, and the resulting decoded object is corrupt.

* Require subclasses to implement init(coder:), even if all it does is abort(). This provides good runtime diagnostics against the bug above, at the cost of some subclass boilerplate. This is what Swift does, and what ObjC now does with the new designated initializer enforcement.

I fear that non-inherited protocols will reintroduce the old bugs. The subclasses will not be required to implement init(coder:), and encoding and decoding an instance of such a class will do corrupt things at runtime with no runtime diagnostics.

You're right, NSCoding is probably a legitimate example of a case where some class hierarchies should enforce refinement by subclasses. Even here, though, there are class hierarchies where a common NSCoding implementation would be appropriate. Do NSString or NSArray subclasses usually provide their own NSCoding implementation?

-Joe

···

On Dec 14, 2015, at 6:53 PM, Greg Parker <gparker@apple.com> wrote:

On Dec 10, 2015, at 18:04, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote: