POP, Generics, Associated Types, and Ponies

So I have a little dependency injection library I wrote called Factory and I was working on a few examples of how to use it and I ran into a problem.

This is, however, going to take a little setup.

All of us, at one point or another, have probably defined a protocol and associated services something like the following:

protocol AccountLoading {
    func load() -> [Account]
}

struct AccountLoader: AccountLoading {
    func load() -> [Account] {
        return [Account()]
    }
}

Note that my protocol and loader return straight, non-async values just to keep the code simple.

With Factory I can register a service that uses both of these for later injection.

extension Container {
    static let accountLoader = Factory<AccountLoading> { AccountLoader() }
}

Now, with the protocol in place, I can also create a mock version and use that for previews, mocking, and testing.

struct MockAccountLoader: AccountLoading {
    func load() -> [Account] {
        return [Account()]
    }
}

func setupMocks() {
    Container.accountLoader.register { MockAccountLoader() }
}

This is all good so far. I have my protocol. I have my service. I have mocks. But...

It would be nice if, instead of manually creating loaders and mock handlers, I could use some spiffy generic service providers. (This is where the problems begin....)

Let's say I have a couple of services like the following:

struct NetworkLoader<T> {
    let path: String
    func load() -> T {
        fatalError()
    }
}

struct MockLoader<T> {
    let data: T
    func load() -> T {
        return data
    }
}

That, in turn should let me revise my Factory and do...

extension Container {
    static let accountLoader = Factory<AccountLoading> {
        NetworkLoader<[Account]>(path: "/api/accounts")
    }
}

But I get an error about how NetworkLoader doesn't conform to AccountLoading... because, well, it doesn't. I get that. I have a load() -> [Account] function, but that doesn't automatically make it conform to the protocol.

So let's add that conformance to the loader and to the mock loader as well.

extension NetworkLoader<[Account]>: AccountLoading {}
extension MockLoader<[Account]>: AccountLoading {}

Success! But... this is a little clunky. I can use my generic services, but I still have to do the following for each and every type I want to work with.

protocol AccountLoading {
    func load() -> [Account]
}

extension NetworkLoader<[Account]>: AccountLoading {}
extension MockLoader<[Account]>: AccountLoading {}

It would be nice if I could bypass the need for all of that. Maybe a generic protocol is in order......

protocol TypeLoading {
    associatedtype T
    func load() -> T
}

extension NetworkLoader: TypeLoading {}
extension MockLoader: TypeLoading {}

And then create my Factory using the generic type...

extension Container {
    static let accountLoader = Factory<TypeLoading<[Account]>> {
        NetworkLoader<[Account]>(path: "/api/accounts")
    }
}

But sadness. Swift doesn't approve of this and tells me that I can't specialize protocol type TypeLoading.

Bummer.

Now, I could do the following...

protocol NewAccountLoading: TypeLoading where T == [Account] {}

extension NetworkLoader<[Account]>: NewAccountLoading {}
extension MockLoader<[Account]>: NewAccountLoading {}

extension Container {
    static let accountLoader = Factory<any NewAccountLoading> {
        NetworkLoader<[Account]>(path: "/api/accounts")
    }
}

But, similar to how things worked earlier, I still have to define the NewXXXLoading protocol and extensions for each and every type I want to use.

The code I need to write is getting smaller, but that's still boilerplate code I'd simply prefer to avoid.

I could ignore protocols altogether and go classic OOP...

class AbstractLoader<T> {
    private init() {}
    func load() -> T {
        fatalError()
    }
}

class NetworkLoader<T>: AbstractLoader <T> {
    // implementation
}

class MockLoader<T>: AbstractLoader <T> {
    // implementation
}

Which at least gives me a clean Factory specification without the need for extra protocol and extension definitions.

extension Container {
    static let accountLoader = Factory<AbstractLoader<[Account]>> {
        NetworLoader<[Account]>(path: "/api/accounts")
    }
}

But in this day and age that feels... well... old. And not very Swifty.

Which brings me, finally, to my question. Is something like the following even possible given the state of Swift generics?

extension Container {
    static let accountLoader = Factory<TypeLoading<[Account]>> {
        NetworkLoader<[Account]>(path: "/api/accounts")
    }
}

What am I missing?

That's all I want for Christmas. A simple definition without extra protocols and extensions.

Well. maybe that and the pony....

BTW, the definition of a Factory could be expressed like this...

public struct Factory<T> {
    let factory: () -> T
    public init(factory: @escaping () -> T) {
        self.factory = factory
    }
    public func callAsFunction() -> T {
        factory()
    }
}

There's a lot more to it, of course, but basically it's a typed struct.

I think you can do this, before Christmas even, if you give your protocol a primary associated type. Though you’d then make that explicit by spelling it any TypeLoading<[Account]>.

2 Likes

Ohhhh. New in 5.7!

protocol TypeLoading<T> {
    associatedtype T
    func load() -> T
}

extension NetworkLoader: TypeLoading {}
extension MockLoader: TypeLoading {}

extension Container {
    static let typedAccountLoader = Factory<any TypeLoading<[Account]>> {
        NetworkLoader<[Account]>(path: "/api/accounts")
    }
}

Unfortunately... "Runtime support for parameterized protocol types is only available in iOS 16.0.0 or newer"

:frowning:

Other ways?

You’re not casting as? any TypeLoading<Whatever>, so there should be no dependency on iOS 16.

May be missing what you're saying, but the following simply gives the same iOS 16 error...

extension Container {
    static let typedAccountLoader = Factory<any TypeLoading<[Account]>> {
        NetworkLoader<[Account]>(path: "/api/accounts") as any TypeLoading<[Account]>
    }
}

The error is on Factory<any TypeLoading<[Account]>>. It's not within the closure.

I’m sorry, my mistake. It looks like you will need a non-generic wrapper for now, even if that wrapper doesn’t do anything:

struct AccountLoading {
  var inner: any TypeLoading<[Account]>
}

let typedAccountLoader = Factory<AccountLoading> {
  AccountLoading(inner: NetworkLoading("api/accounts"))
}

Not ideal, but possibly still better than repeating the protocol?

1 Like

The wrapper doesn't need to be non-generic, for what it's worth; you could have:

struct AccountLoading<T> {
  var inner: any TypeLoading<T>
}

The constraint is just that you can't use a parameterized protocol type as a generic argument itself without the runtime support.

3 Likes

May be the best choice. That's not really just an AccountLoader though...

protocol TypeLoading<T> {
    associatedtype T
    func load() -> T
}

struct AnyLoader<T> {
    let wrapped: any TypeLoading<T>
    init(_ wrapped:  any TypeLoading<T>) {
        self.wrapped = wrapped
    }
    func load() -> T {
        wrapped.load()
    }
}

extension Container {
    static let accountLoader = Factory<AnyLoader<[Account]>> {
        AnyLoader(NetworkLoader(path: "/api/accounts"))
    }
}

At least this way I don't have to redefine a new protocol each and every time I want to use it.

I sort of feel like I got my pony... but now have to wait a couple of years before he's old enough to ride...

What about foregoing protocols and just making TypeLoading a datatype wrapper around a function?

struct Account {}

struct TypeLoading<T> {
    var load: () -> T
}


public struct Factory<T> {
    let factory: () -> T
    public init(factory: @escaping () -> T) {
        self.factory = factory
    }
    public func callAsFunction() -> T {
        factory()
    }
}

struct NetworkLoader<T> {
    let path: String
    func load() -> T {
        fatalError()
    }
}

extension NetworkLoader where T == [Account] {
    static let accounts = NetworkLoader(path: "/api/accounts")
}

struct MockLoader<T> {
    let data: T
    func load() -> T {
        return data
    }
}

struct Container {}

extension Container {
    static let accountLoader = Factory<TypeLoading<[Account]>> {
        .init(load: NetworkLoader.accounts.load)  //The .init is TypeLoading<[Account]>.init
    }
}

Umm... if you're going to do that then you don't even need the wrapper.

typealias LoadFunction<T> = () -> T

extension Container {
    static let accountLoader = Factory<LoadFunction<[Account]>> {
        NetworkClassLoader<[Account]>(path: "/api/accounts").load
    }
}

The parameterized protocol type is closest to what I want to do... but in typical Apple fashion I just can't do it yet.