Self referencing associated type

In an attempt to make abstraction of the following code:

protocol EntityLoader {
    associatedtype EntityType
    func loadNext(with url: URL, completion: @escaping ([EntityType]) -> ())
}

a concrete entity type is defined:

enum ImageEntity<T: EntityLoader> where T.EntityType == ImageEntity {
    case image(NSImage)
    case placeHolder(URL)
    ...
    func load(with loader: T, completion: @escaping ([ImageEntity]) -> () {
         ....
    }
}

However, in order to implement a concrete loader for the above,

class ConcreteLoader: EntityLoader {
    typealias EntityType = ImageEntity< ConcreteLoader >
    func loadNext(with url: URL, completion: @escaping ([EntityType]) -> ()) {}
    ....
}

The compiler would refuse to compile with error: error: type alias 'EntityType' references itself.

Is there anything wrong with my definition of abstract types?

Not sure if there should be an error or not, ie whether satisfying the constraints would require an infinitely recursive definition of your ImageEntity<…∞…> or not.

But the following program, which is an alternative formulation of your code, crashes the compiler (default toolchain of Xcode 10):

import AppKit

protocol EntityTypeProtocol {
    associatedtype EntityLoader: EntityLoaderProtocol
        where EntityLoader.EntityType == Self
}

protocol EntityLoaderProtocol {
    associatedtype EntityType: EntityTypeProtocol
        where EntityType.EntityLoader == Self
    
    func loadNext(with url: URL, completion: @escaping ([EntityType]) -> ())
}

enum ImageEntity<Loader>: EntityTypeProtocol
    where Loader: EntityLoaderProtocol
{
    case image(NSImage)
    case placeHolder(URL)

    func load(with loader: Loader,
              completion: @escaping ([ImageEntity]) -> ())
    {
        // ...
    }
}

class ConcreteLoader: EntityLoaderProtocol {
    
    typealias EntityType = ImageEntity<ConcreteLoader>
    func loadNext(with url: URL, completion: @escaping ([EntityType]) -> ()) {
        // ...
    }
}

The above program can be reduced to:

protocol P {
    associatedtype A: Q where A.B == Self
}

protocol Q {
    associatedtype B: P where B.A == Self
}

struct S1<T>: P where T: Q { }

struct S2: Q {
    typealias B = S1<S2>
}

which also crashes the compiler. Reported as SR-8998.

2 Likes

Since you're having to pass an instance of the loader anyway, I would recommend making the function generic rather than the enum:

(Tested in Swift 4.1 playground)

protocol EntityLoader {
    associatedtype EntityType
    func loadNext(with url: URL, completion: @escaping ([EntityType]) -> ())
}


enum ImageEntity  {
    case image(NSImage)
    case placeHolder(URL)
    
    func load<T: EntityLoader> (with loader: T, completion: @escaping ([ImageEntity]) -> ()) where T.EntityType == ImageEntity {
        //  ...
    }
}

class ConcreteLoader: EntityLoader {
    typealias EntityType = ImageEntity
    func loadNext(with url: URL, completion: @escaping ([ImageEntity]) -> ()) {
        // ...
    }
}

It's unclear how you're wanting to use this, but one nice possibility might be having a generic loader:

class GenericLoader<T>: EntityLoader {
    typealias EntityType = T
    func loadNext(with url: URL, completion: @escaping ([EntityType]) -> ()) {}
}

Then have Type specific extensions if needed:

extension GenericLoader where T == ImageEntity {
    func generateEntity(from data: Data) -> ImageEntity? {
        //...
        return nil
    }
}

The same-type constraint implies a recursive definition on the generic parameter. Notice how T.Assoc == Enum below is equivalent to T.Assoc == Enum<T>.

protocol P {
    associatedtype Assoc
}

enum Enum<T: P> where T.Assoc == Enum {}

class Foo: P { // Type 'Foo' does not conform to protocol 'P'
    typealias Assoc = Enum<Foo>
}

There's nothing wrong in having a recursive type alias, but apparently Swift currently doesn't support those through generics. It becomes clear if you explicitly mention the generic parameter in any way, and I think the the real problem here is that the error isn't triggered unless you actually mention it:

enum Enum<T: P> where T.Assoc == Enum<T> {} // Generic enum 'Enum' references itself

On its turn an error that isn't triggered on time forces the compiler to fall back to more generic errors like does not conform to protocol.

Terms of Service

Privacy Policy

Cookie Policy