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()
}
}
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()
}
}
}
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.
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).
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()
}
}
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.
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.
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