Returning a concrete type conforming to a protocol from a method when an existential of this protocol is expected

Consider I have this abstract base class with one virtual method with a return type of Foo:

class Foo {
    func foo() -> Foo { fatalError("abstract") }
}

Because Foo is in covariant position, a subclass can return an instance of a subclass of Foo, like this:

final class SubFoo: Foo {
    let value: Int = 0
    
    override func foo() -> SubFoo {
        return SubFoo()
    }
}

This is convenient because when a given instance is statically known to be of type SubFoo, then .foo() will return an instance of type SubFoo. At the opposite, when an instance of a subclass of Foo is erased to Foo, then .foo() returns a type-erases Foo:

let subFoo = SubFoo().foo()  // Statically known to be of type `SubFoo`
let foo = (SubFoo() as Foo).foo()  // `foo` is erased to `Foo`

My issue is that I cannot find a way to replicate this nice behavior with protocols and existentials:

protocol Bar {
    func bar() -> any Bar
}

struct ImplBar: Bar {
    var value: Int = 0
    
    func bar() -> ImplBar {  // Error: returning `ImplBar` not allowed
        return ImplBar()
    }
}

Is there a way to make the protocol version to work or this behavior is not supported by Swift protocols/existentials?

protocol Bar {
  func bar() -> Self
}

Maybe not quite as ergonomic as you'd want (see pitfalls), but I think it does the job?

Have you considered adding an associated type to Foo as a protocol? In Swift 5.7 you should get the same behavior you got via subclassing:

protocol Foo {
    associatedtype Return: Foo
    func foo() -> Return
}

struct SubFoo: Foo {
    let value = 0

    func foo() -> SubFoo {
        return SubFoo()
    }
}

let subFoo = SubFoo().foo()  // Statically known to be of type `SubFoo`
let foo = (SubFoo() as any Foo).foo()  // `foo` is erased to `any Foo`
2 Likes

This would work in this specific case, yes, but it does not generalize unfortunately:

protocol Baz { }
struct ImplBaz: Baz { }

protocol Bar {
    func bar() -> any Baz
}

struct ImplBar: Bar {
    var value: Int = 0
    
   // Not possible to return a concrete type instead of a type-erased `Baz`
    func bar() -> ImplBaz { 
        return ImplBaz()
    }
}
1 Like

I just tried it using Swift 5.7 and I think it works as intended!