Satisfying protocol requirement with specific type

I came across this issue when porting a Java project to Swift. Apologies if it has been asked before (I am sure it must) but I lack the Google foo to search for appropriate prior posts.


Consider the following:

protocol Child
{
}

protocol Parent
{
	func foo() -> Child
}

class B: Child
{
}

class A: Parent // Error - class A does not conform to protocol 'Parent'
{
	func foo() -> B
	{
		return B()
	}
}

If I change the return type of foo() in A to be Child it compiles fine. Why does the compiler not consider the original func foo() -> B to fulfil the contract func foo() -> Child when a B is clearly a Child?

The reason I don't want to change the return type to Child is because the Java equivalent in the project I am porting compiles fine and code internal to A automatically knows that foo() returns a B and exploits that fact.


Bonus question: why does autocorrect on my Mac think func is meant to be "functionalists"?

2 Likes

What you're doing is the same as something like:

protocol Foo {
  func view() -> UIView
}

class MyView: UIView {}

class Bar: Foo {
  // error: Type 'Bar' does not conform to protocol 'Foo'
  func view() -> MyView { fatalError() }
}

If you want covariance, you need to use an associatedtype.

protocol Parent {
  associatedtype C: Child
  func foo() -> C
}

Another way to think about it is to fix the error:

class A: Parent 
{
	func foo() -> B
	{
		return B()
	}
    func foo() -> Child
    {
        return … some Child …
    }
}

This is valid, though not very useful. If this is valid, a function returning a B can't be what satisfies the protocol.

Sorry, but I don't understand why your example shouldn't be OK (apart form the fatalError). MyView is clearly a subclass of UIView and so can be used anywhere a UIView is used.

I can think of lots of ways to "fix" it. I want to know why it isn't allowed.

Right... it's something that should be supported (at least IMO), it's just that it's not at the moment. See https://bugs.swift.org/browse/SR-522

There's potential for breaking source compatibility and potentially leading to dangerous behaviour, so I am not sure how likely it is that this will be fixed. But it seems like lots of people want it, so maybe there's a chance the core team will allow it.

The OP's example won't be supported even with covariant returns and is unrelated to your example. Returning a value of type Child and returning a value of type B are totally different. B conforms to the protocol Child, while the existential type Child does not conform to Child.

Sorry, I was referring to my own example (when I said I think it should be supported), not OP's example as I was replying to the OP questioning why my example isn't OK at the moment.

That doesn't seem to make any sense. The contract is that I have to return something that conforms to the Child protocol. class B conforms to the Child protocol. Why doesn't a function that returns a B satisfy the requirement that it return something that conforms to Child?

No, the contract is that you have to return something of concrete type Child, which pointedly does not conform to the Child protocol.

1 Like

By the way, my name is "jeremyp". It's weird seeing everybody refer to "the OP". You can call me by my name and if you are concerned about the pronoun, "he" is fine.

What concrete type Child? I defined a protocol called Child not any concrete type.

A protocol without Self or associated type requirements can be used as a concrete type; that type is called a "protocol type" in some places in Swift, but is otherwise known as an "existential type." There is a ton to know about these types, but I will quote the following paragraph by way of background:

In the future, it would be possible for Child to conform to itself because there are no Self or associated type requirements. But even in that future, B (a concrete type) would not have a subtyping relationship with Child (another concrete type) any more than it would with another hypothetical class C that conforms to Child.

Your protocol Parent demands a function that returns a value of concrete type Child. Returning a value of type B does not and will never satisfy that requirement. To create a contract for a function that "returns something that conforms to the Child protocol," use an associated type constraint in your protocol.

2 Likes

Yeah, this is a bit rubbish. Not your explanation, which is excellent, thank you, but the whole hidden concrete type nonsense.

Any reasonable person seeing func foo() -> Child would assume it could return anything that conforms to the Child protocol. There's nothing in the code above that even hints at the fact that a new concrete type has been automagically created behind the scenes. This is an implementation detail that has surfaced in the programmer's model.

I would argue that is counter intuitive to most people looking at the code coming from languages other than Swift. Even to me who has been programming in Swift since version 1 it looks like "your function can return anything as long as it conforms to Child"

That would make things a lot more painful. The real example has much more complex type relationships. I'd suddenly have generics multiplying all over the place that don't need to exist in the original code. I'll figure out a hack instead.

The simplest way to comply with the protocol as you have defined it is :

protocol Child
{
}

protocol Parent
{
  func foo() -> Child
}

class B: Child
{
}

class A: Parent
{
  func foo() -> Child
  {
    return B()
  }
}

The contract you specified in your Parent protocol is to return something that is a Child. The implementation, as I have written it, does exactly that.

If you want your conforming type to explicitly return a type that conforms to Child, then you need to use an associated type to tell the compiler that is your intention with an associated type :

protocol Child
{
}

protocol Parent
{
  associatedtype ChildType : Child
  
  func foo() -> ChildType
}

class B: Child
{
}

class A: Parent
{
  func foo() -> B
  {
    return B()
  }
}

And, whatever you do, never try arguing "well it works differently to Java". Swift is not another language, it is what it is. I have worked in (at least) COBOL, C, Pascal, C++, dBase, Clipper, Delphi, C#, Objective-C and Swift. Most of my work is teaching developers how to design frameworks, which involves implementing Design Patterns. Some design patterns are immediately realisable in most languages, others are not, which is when you have to either work within the constraints of a language or change to another.

Well, it can. That isn’t what’s at issue at all. We are talking about the return value’s static type, not its dynamic type (type(of: foo() as Any)).

The static type is declared to be Child, which is a perfectly reasonable thing to do. A method can return anything that conforms to Child, at its discretion, which can vary from one invocation to the next at runtime.

This is distinct from a method that always returns a particular type conforming to Child that is knowable statically. That particular type might be knowable statically to:

  • the caller but not the implementor (generic <T: Child> () -> T),
  • the implementor but not the caller (“opaque” or “reverse generic” () -> some Child), a new feature that could be potentially made more general in the future with the notation () -> <T: Child> T,
  • both the implementor and the caller (() -> B),
  • or—for the sake of completeness even though I am repeating myself—neither the implementor nor the caller (() -> Child).

A protocol, in turn, can require the static type of the return value to be known by the type’s caller and not chosen by the type’s implementor by requiring the use of generics. A protocol cannot yet require the use of “reverse generics” but I suspect it will eventually be possible to do so. Or a protocol can require the type to be known to both parties or to neither party. This is distinct from whether the protocol itself puts any constraints on what that type should be (an associated type).

Not all type relationships are “generics.” Protocols with associated types are not generic protocols (which are unlikely ever to happen). Again, to appreciate the difference, ask yourself the question: the type in question is meant to be known statically to the person who is writing what code?

The point of having all of these features in a statically typed language is to allow you to model these type relationships so that the type checker can help guarantee certain invariants at compile time. If this isn’t what you want, and you’re looking for polymorphism to be a runtime concept only, then Swift may not be the right tool for the job.

2 Likes

Do you can use some or did I misunderstand some?

protocol Child { }
protocol Parent {
    associatedtype C: Child
    func foo() -> C
}

class B: Child { }
class A: Parent {
    func foo() -> some Child {
        return B()
    }
}

That’s totally doable, just not exactly what the discussion is about (or is it :thinking:).

It’s how you do things in SwiftUI (View, Gesture, etc.).

Terms of Service

Privacy Policy

Cookie Policy