sak sak - 27 days ago 11
Swift Question

Error using Self in generic function where clause

I am running into an issue when I try to use

Self
as part of a
where
clause in a generic function which is part of a protocol.

For example, say I have this protocol and this generic function defined:

protocol Animal {
associatedtype FoodSource
func eat(_ food:FoodSource)
}

// The where clause specifies that T2 must conform to
// whatever type is T1's FoodSource associated type
func feed<T1: Animal, T2>(animal:T1, food:T2) where T2 == T1.FoodSource {
animal.eat(food)
}


The function Feed uses the bracketed statement to declare that the first parameter must conform to the
Animal
protocol. It uses the
where
clause to declare that the second parameter's type must conform to the associated type of the first parameter.

It's possible to create classes which conform to this generic function's requirements and everything works perfectly. For example:

protocol Meat {}
protocol Vegetable {}

class Rabbit : Animal {
typealias FoodSource = Vegetable
func eat(_ food:FoodSource) {
print("the Rabbit ate the \(type(of:food))")
}
}

class Lion : Animal {
typealias FoodSource = Meat
func eat(_ food:FoodSource) {
print("the Lion ate the \(type(of:food))")
}
}

class Carrot : Vegetable {}
class Steak : Meat {}
class ChickenSalad : Meat, Vegetable {}

// works because Carrot conforms to Vegetable
// prints: "the Rabbit ate the Carrot"
feed(animal: Rabbit(), food: Carrot())

// works because Steak conforms to Meat
// prints: "the Lion ate the Steak"
feed(animal: Lion(), food: Steak())

// works because ChickenSalad conforms to Meat
// prints: "the Lion ate the ChickenSalad"
feed(animal: Lion(), food: ChickenSalad())

// works because ChickenSalad conforms to Vegetable
// prints: "the Rabbit ate the ChickenSalad"
feed(animal: Rabbit(), food: ChickenSalad())


So far so good.

However, when I implement the same pattern of generics as part of a protocol, it no longer works:

protocol Food {
func feed<T:Animal>(to:T) where Self == T.FoodSource
}

extension Food {
func feed<T:Animal>(to animal:T) where Self == T.FoodSource {
animal.eat(self)
}
}

class SteakSalad : Food, Meat, Vegetable {}

SteakSalad().feed(to: Lion())


When executed, this block throws the following error:

error: generic parameter 'T' could not be inferred
SteakSalad().feed(to: Lion())
^


Is there some way to achieve the desired behavior?

Answer Source

Before discussing this, I highly advice you to rethink your problem and simplify your types. Once you wander down the road of mixing generics and protocols in Swift, you will be fighting the type system nonstop. Part of this is that complex types are complex and it is difficult to get them correct even with a very strong type system. Part of it is that Swift does not have a very strong type system. Compared to Objective-C or Ruby, sure, it's incredibly powerful, but it's still fairly weak around generic types and there are many concepts you can't express (there are no higher-kinded types, there is no way to express covariance or contravariance, no dependent types, and there are weird quirks like protocols don't always conform to themselves). In nearly every case where I've worked with a developer on their complex types, it turned out that their actual program didn't need so much complexity. Protocols with associated types should be considered an advanced tool; don't reach for them unless you really need them. See Beyond Crusty for more on that.

This doesn't work because it violates your where clause:

func feed<T:Animal>(to:T) where Self == T.FoodSource

So Animal.FoodSource has to match Self. Let's look at how you use it:

SteakSalad().feed(to: Lion())

So Self is SteakSalad and Lion.FoodSource is Meat. Those are not equal, so this doesn't apply. What you really mean is this:

func feed<T:Animal>(to animal:T) where Self: T.FoodSource

But that isn't legal Swift ("error: first type 'T.FoodSource' in conformance requirement does not refer to a generic parameter or associated type"). The problem is that T.FoodSource could be anything; it doesn't have to be a protocol. "Self conforms to an arbitrary type" is not meaningful Swift.

We could try to improve this by making FoodSource at least conform to Food, but it gets even worse:

protocol Food {}
protocol Meat: Food {}

protocol Animal {
    associatedtype FoodSource: Food
}

And then make Lion eat Meat:

class Lion : Animal {
    typealias FoodSource = Meat

MyPlayground.playground:1:15: note: possibly intended match 'Lion.FoodSource' (aka 'Meat') does not conform to 'Food'

typealias FoodSource = Meat

Doesn't Meat conform to Food? Hah, no. This is part of the greater "protocols don't conform to themselves" limitation in Swift. You can't just treat protocols like they have inheritance. Sometimes they do, and sometimes they don't.

What you can do is that that meat can be fed to meat-eaters:

protocol Meat {}

extension Meat {
    func feed<T:Animal>(to animal:T) where T.FoodSource == Meat {
        animal.eat(self)
    }
}

And Veg can be fed to veg-eaters:

protocol Vegetable {}

extension Vegetable {
    func feed<T:Animal>(to animal:T) where T.FoodSource == Vegetable {
        animal.eat(self)
    }
}

But there's no way I know of to make this generic over protocols-with-associated-types (PATs). It's just too much for the Swift type system. My recommendation is to get rid of the PAT and just use generics. Most of these problems will go away. Even in a language like Scala that has a more powerful type system and also has associated types, the right answer is usually simpler generics (and often not even that; we often make things generic when there's no need).