Generic Protocols

Is it possible to implement generic protocols? If so how?

I tried to write something like this:

protocol Foo<Item> {
   associatedtype Item
   foo(foo: Item)
}

protocol Bar<Item> {
  associatedtype Item
  foo(foo: Item)
}

class Buz: Foo<Int>, Bar<String> {
   foo(foo: Int) {}
   foo(foo: String) {}
}

But somehow it complains that Buz cannot implement the protocol because Item is ambigously set to Int and String.

It seems like associated types aren't namespaced so Item is set on Buz instead of being constrained to the protocol implementation. Like here the two foo methods shouldn't be ambiguous because there can be only one matching either of the protocols.

It also means it's not possible to implement the same protocol but with different associated types like this?

class Baz: Foo<Int>, Foo<String> {
}

Is there a way to do something similar to this. I've tried with Swift 5.10 without success.

1 Like

From the perspective of the type system that has no sense: you are trying to confirm two protocols, both specifying Item type, which you define as different values. That is makes no sense for the type system. In general, you should be able to implement two generic protocols if they as different associated types that won’t conflict with each other.

From the perspective of the language there should be only one function with Item type in parameters list. You think it will behave as specialization, but it won’t. I actually don’t like this interchangability of associatetedtype and actual type on the implementation side — it is point of confusion sometimes.

This compiles on swift.godbolt.org under the nightly toolchain:

protocol Foo<Item> {
   associatedtype Item
   func foo(foo: Item)
}

protocol Bar<Item> {
  associatedtype Item
  func foo(foo: Item)
}

class Buz: Foo, Bar {
   @_implements(Foo, Item)
   typealias FooItem = Int

   @_implements(Bar, Item)
   typealias BarItem = String

   func foo(foo: Int) {}
   func foo(foo: String) {}
}
5 Likes

Your example is completely valid and sound, but you're very close to a soundness hole that I'll explain. In fact, I must congratulate you because your setup with @_implements is more concise than my previous formulation that used conditional conformances! :slight_smile:

The problem is this. If you declare a function that's generic over both Foo and Bar,

func f<T: Foo & Bar>(_ t: T) -> T.Item {...}

There is only one type parameter T.Item. This is inherent to Swift's generics system. It can be derived in one of two ways, from either conformance requirement. The problem is we don't check that the two conformances define coherent type witnesses in this case. So inside f(), we might call a requirement of either Foo or Bar, expecting to produce or return an Item, assuming an Item of Foo has the same concrete type as an Item of Bar, when this is not the case. Undefined behavior results.

Usually, it's hard to define inconsistent type witnesses for the same associated type, because the usual redeclaration rules prevent you from doing this:

extension S: P1 {
  typealias A = Int
}

extension S: P2 {
  typealias A = String // nope
}

But if you play tricks with @_implements, conditional conformances, or resilience boundaries, you can get this setup, and accidentally write code that's generic over it in a way that's not checked, and crash.

I have a plan to completely define this away (except in the retroactive resilience case, where it's unavoidable), at which point the compiler will diagnose incorrect usages of this kind of thing. Until then, I would suggest not doing anything with @_implements and associated type witnesses, because it's not completely checked.

20 Likes

If there can only ever be one T.Item, then that seems to preclude things like Rust's From<T> trait where you're expected to implement it separately for each type that you can be created from. I'm sure you mentioned a different way to handle things like that in an earlier conversation, but I can't remember it.

This is a very odd example as Foo and Bar are just the same protocol with a different name. You could just write:

class Buz: Foo<Int>, Foo<String> {
   func foo(foo: Int) {}
   func foo(foo: String) {}
}

But that doesn't solve the problem because it would still not compile as Swift generics don't work with protocols in the first place.


I actually ran into this issue yesterday, so here is a more concrete example:

Swift version
public protocol HardwareDrawable<Renderer>: Drawable {
    associatedtype Renderer: HardwareRenderer
    func draw(into renderer: inout Renderer, x: Int, y: Int)
}

public protocol HardwareRenderer {}

public extension HardwareRenderer {
    mutating func draw(_ drawable: some HardwareDrawable<Self>, x: Int, y: Int) {
        drawable.draw(into: &self, x: x, y: y)
    }
}

public struct SDLRenderer: HardwareRenderer {}
public struct OpenGLRenderer: HardwareRenderer {}

extension Rectangle: HardwareDrawable {
    public func draw(into renderer: inout OpenGLRenderer, x: Int, y: Int) { ... }
}

extension Rectangle: HardwareDrawable {
    public func draw(into renderer: inout SDLRenderer, x: Int, y: Int) { ... }
}

Rust version
pub trait HardwareDrawable<Renderer: HardwareRenderer>: Drawable {
    fn draw(&self, renderer: &mut Renderer, x: usize, y: usize);
}

pub trait HardwareRenderer {
    fn draw(&mut self, drawable: &mut impl HardwareDrawable<Self>, x: usize, y: usize) where Self: Sized {
        drawable.draw(self, x, y)
    }
}

pub struct SDLRenderer;
pub struct OpenGLRenderer;

impl HardwareRenderer for SDLRenderer {}
impl HardwareRenderer for OpenGLRenderer {}

impl HardwareDrawable<SDLRenderer> for Rectangle {
    fn draw(&self, renderer: &mut SDLRenderer, x: usize, y: usize) { ... }
}

impl HardwareDrawable<OpenGLRenderer> for Rectangle {
    fn draw(&self, renderer: &mut OpenGLRenderer, x: usize, y: usize) { ... }
}

It's kind of a weird protocol; to work within the constraints of a hardware renderer every hardware drawable type needs a custom implementation for every supported renderer, so for example you could imagine the SDLRenderer implementation of Rectangle to be:

import SDL

public struct SDLRenderer: HardwareRenderer {
    internal let handle: OpaquePointer
}

extension Rectangle: HardwareDrawable { // This will error if HardwareDrawable is already implemented for a different renderer.
    public func draw(into renderer: inout SDLRenderer, x: Int, y: Int) {
        var rect = SDL_Rect(x: Int32(x), y: Int32(y), w: Int32(self.width), h: Int32(self.height))
        SDL_SetRenderDrawColor(renderer.handle, self.color.r, self.color.g, self.color.b, self.color.a)
        SDL_RenderFillRect(renderer.handle, &rect)
    }
}

This is the simplest way I can think of to make graphics/layout code completely platform independent while still optimizing for each rather than falling back to drawing pixel by pixel.


I suppose in this case I can sort of get around the issue by declaring new protocols every time, such as SDLHardwareDrawable, but that doesn't enforce a standard api and is just boilerplate (and there will be more generic functionality I would have to duplicate each time)

Is there some workaround I could use here? Maybe a different way to express this that I didn't consider?
Either way, wouldn't it make sense for Swift to eventually support generic protocols? It seems like a useful feature.

A concrete type can only conform to a protocol once, so Swift doesn’t have anything like this.

I think this problem could be solved easily by using something similar to this.

func f<A: Foo, B: Bar, T: A & Br>(_ t: T) -> A.Item {...}

Here we have more or less the same thing but we can specialize the return type based on the generic arguments. Swift could include a syntax to specialize from a generic type but it's not exactly necessary here.

One other problem is if we were to reuse an existing protocol. One example is how the From trait from Rust.

It seems impossible to reproduce that behaviour while it would be super useful.

For example here, there is still no confusion or ambiguity.

func f<A: Foo<Int>, B: Foo<String>, T: A & B>(_ t: T) -> A.Item {...}

The return type would need to be the Item of Foo<Int> which is already specific enough to be deduced.

But nothing can be Foo<Int> & Foo<String>, so this is ill-formed -- nevermind the fact that you currently can't constrain one generic type parameter to be a subtype of another.

1 Like

That's pretty much why I asked if it's feasible. Coming from Rust, each implementation of a trait (somewhat similar to a protocol) are their own custom type living in parallel with the struct implementing them.

A trait that is non ambiguous can technically be called directly on a struct without specializing it. But each implementation of a trait even generic traits are completely separated types. In Swift, from my examples, everything is pushed in the same namespace which cause issues with name clashing of associated types and function names.

In Rust, you can always get a specialized function to a very specific type since they're always separate things. There are some issues with generic associated types that swift doesn't seem to have but swift has its own limitations unfortunately.

For example, in Rust you can have the trait Into<T> implemented multiple times. It's a core feature of rust that makes it possible to have generic functions without the need of overloading functions like init(x: String), init(x: Int). You can simply have init<T: Into<Obj>>(x: T) which ensure that T can be converted into Obj regardless of it being a String, Int, or anything that implements the trait.

Instead of extending a class through extension, we write generic interface that can be called with generic types. So instead as long as you can implement the trait/protocol, you can make a function callable by new types.

That's nice, I'll have to try this. It's unfortunate it's a bit cumbersome to do something that should be simple.

Okay, I think I found my answer here after searching around.