Instantiate associatedtype from outside of protocol

In a framework I'm creating I have

protocol Game {
    associatedtype Options: GameOptions

    init()
}

protocol GameOptions {
    init()
}

struct Configuration {
    let gameType: any Game.Type
    let gameOptions: any GameOptions.Type
    
    func setup() {
        let game = gameType.init()
        let options = gameOptions.init()
    }
}

To reduce the number of fields in Configuration, I was wondering if I can replace the line

let options = gameOptions.init()

with something like

let options = gameType.Options.init()

which currently results in a compiler error

Type of expression is ambiguous without a type annotation

Swift can’t express the type of game.Options because it would depend on the value of game. To work around this, you could create a helper on Game that just calls through to Options.init():

protocol Game {
    associatedtype Options: GameOptions

    init()
    fileprivate func _makeOptions() -> any GameOptions() { Options.init() }
}

protocol GameOptions {
    init()
}

struct Configuration {
    let gameType: any Game.Type
    let gameOptions: any GameOptions.Type
    
    func setup() {
        let game = gameType.init()
        let options = gameType._makeOptions()
    }
}
3 Likes

You might also want to consider making Configuration generic over the Game instead:

struct Configuration<G: Game> {
  let gameType: G.Type
  let gameOptions: G.Options.Type
}
6 Likes

You can't nest types inside of protocols. But don't let that force you into associated types if the equivalent of them would do, when the general case that Slava went over doesn't apply.

protocol Game {
  init()

  typealias Options = MyLibrary.Options<Self>
  typealias Configuration = MyLibrary.Configuration<Self>
}

struct Options<Game: MyLibrary.Game> { }

struct Configuration<Game: MyLibrary.Game> {
  func setUp() {
    let game = Game()
    let options = Game.Options()
  }
}

Do you even need a Game protocol?

struct Game {
  struct Options { }
  struct Configuration {
    func setUp() {
      let game = Game()
      let options = Options()
    }
  }
}
1 Like

I was expecting gameType.Options (which I used in my sample code), or even game.Options (as you mentioned, and perhaps you got confused by the similar variable names) to return any GameOptions, which is actually what I want (I wasn't expecting a concrete type). Shouldn't the compiler be able to do that? That's basically what you're doing in _makeOptions(), right?

That's not possible in my case, since both Game and Configuration are part of the framework, which expects the client to pass a Configuration object to it.

Why not? The compiler knows the protocol type, which is exactly what I need.

Yes. I just simplified the sample code for this question.

Yes, I think looking up an associated type member of a metatype should do that. Or at least the poor diagnostic you observed should be fixed. Do you mind filing an issue?

A good bit of philosophical advice is to design your architecture around what the type system can express, instead of trying to go the other way around.

There will always exist “reasonable” patterns that a static type checker cannot “prove”, pretty much because of the halting problem.

3 Likes

Do you mean on GitHub? I filed Accessing associatedtype from outside of protocol · Issue #76201 · swiftlang/swift · GitHub

Sure. My code works pretty well like this, I was just wondering if I could make it a little simpler. I was actually using generics before, but then migrated to protocols, partly because of some limitations of generics (for instance not being able to define static properties in a subclass of a generic class, which I can do in a protocol implementation) and also because the subclass of the generic class was expected to implement some methods (which just contained a fatalError() call in the generic class, which now the protocol implementation must define).

1 Like

All this talk of existentials and subclassing sounds very Objective-C to me, which would probably make me shy away from whatever framework this is, but you can get any GameOptions.Type without adding to the Game protocol.

struct Configuration {
  let gameType: any Game.Type
  var gameOptionsType: any GameOptions.Type {
    func options<Game: MyLibrary.Game>(_: Game.Type) -> Game.Options.Type {
      Game.Options.self
    }
    return options(gameType)
  }

  func setUp() {
    let game = gameType.init()
    let options = gameOptionsType.init()
  }
}
1 Like

The framework allows the client to create a board game by taking a concrete game implementation and offering many features around it, like player management. I used to do it with subclasses, but then converted them to protocols which the client has to implement, but then the client has to let the framework know how to instantiate the game itself, which is why I'm using Configuration which contains the game class type. Do you see a better way of doing it?

Wow, thank you. I guess this is a workaround, right? I don't see why this would work but my initial code wouldn't.

2 Likes

Sorry, I can't wrap my head well enough around what you're saying here:


Oh, me neither. There might be some other trick hidden in the Implicitly Opened Existentials proposal, but what I showed you is the best I've been able to come up with.

1 Like

Yeah, it's just an oversight in the implementation.

This works:

struct S {
  typealias A = Int

  static var a: A.Type { return A.self }
}

let s = S.self
let sa: S.A.Type = s.A.self
let sa2: S.A.Type = s.a

sa and sa2 do the same thing. Both are just a long-winded way of saying S.A.self.

Now if I add an existential to the mix, spa2 still works, but spa does not:

protocol P {
  associatedtype A: Equatable
}

extension P {
  static var a: A.Type { return A.self }
}

extension S: P {}

let sp: any P.Type = S.self
let spa: any Equatable.Type = sp.A.self
let spa2: any Equatable.Type = sp.a

However the associated type declaration A actually has the same interface type as the computed property P.a in the protocol extension. So while swift-evolution/proposals/0309-unlock-existential-types-for-all-protocols.md at main · swiftlang/swift-evolution · GitHub doesn't explicitly call out this case, reading between the lines it was obviously the intent that this should work.

4 Likes