Hi,
I've been toiling away to try to solve this architectural/academic problem (depending on your use case).
I feel like it should be possible to use the type system to pass in a collaborator type C to a function on type A, such that type C is constrained in some way to type A — but the function on A that is passed a C is defined in a protocol.
This seems to be impossible in current Swift 4.x
Here's some sample code that demonstrates one attempt at this, as a playground you can paste in and run.
The goal of this code is to define an API for statically declaring types of Robot
, some of which are Programmable
.
- These
Programmable
robots can statically define a list of available command bindings, by implementing the function from the protocol - The builder that is passed these bindings should only accept command bindings that bind to the exact type A
- The bindings are gathered at runtime, but we should be able to ensure at compile time that it is not possible to pass a binding from another Robot type B to A's command builder.
It seems to be a challenging/insurmountable challenge with current Swift. I'm very interested to hear any ideas / explanations for the limitation. I'm not interested in "Why do you even what to do this" — if that isn't clear from the above don't sweat it and please move on :)
//
// Paste this code into a playground to see the compiler error and have a shot at a solution.
// All suggestions gratefully received. This is really an exercise in seeing if there is a way to lock down
// intermediary command "binding" types to only those that relate to the same kind of Robot as the CommandBuilder expects.
//
// There is a compiler error when we try to add the actual binding to the builder:
//
// Collaborator with Constrained Self Conformance.playground:63:21: error: cannot convert
// value of type 'CommandBinding<MetalMickey, DanceCommand>' to expected argument type 'CommandBinding<_, _>'
// builder.add(dance)
// ^~~~~
import UIKit
protocol Command {
associatedtype InputType
static var name: String { get }
}
protocol Robot {
static var description: String { get }
}
class CommandBuilder<R> where R: Robot & Programmable {
var commandMappings: [String:Any] = [:]
func add<C>(_ commandBinding: CommandBinding<R, C>) {
commandMappings[commandBinding.command.name] = commandBinding
}
}
/// Define a convention that programmable robots implement to build their command
/// list when passed a builder created to only contain commands that apply to their own type
protocol Programmable {
static func defineCommands<R>(_ builder: CommandBuilder<R>) where R: Robot
}
class CommandBinding<R, C> where R: Robot, C: Command {
let robot: R.Type
let command: C.Type
init(robot: R.Type, command: C.Type) {
self.robot = robot
self.command = command
}
func displayText() -> String {
return "\(command.name) on \(robot.description)"
}
}
/// Provide a function that allows the programmable robots to construct a generic builder
/// specific to their own Self type
extension Programmable where Self: Robot {
// Result is Any for simplicity of example
static func collectCommands() -> [Any] {
let builder = CommandBuilder<Self>()
// Get Self (a Programmable) to define the commands
defineCommands(builder)
return Array(builder.commandMappings.values)
}
}
// Using the API
class DanceCommand: Command {
typealias InputType = String
static var name: String = "dance"
}
class MetalMickey: Robot, Programmable {
static var description: String = "Metal Mickey"
static let dance = CommandBinding(robot: MetalMickey.self, command: DanceCommand.self)
static func defineCommands<R>(_ builder: CommandBuilder<R>) where R: Robot & Programmable {
/// !!! Here's the problem
builder.add(dance)
}
}
/// Our app might iterate over a lot of these types
let allRobotTypes: [(Robot & Programmable).Type] = [
MetalMickey.self
]
func buildCommandsFor(_ robot: Robot.Type) {
if let programmableRobot = robot as? (Robot & Programmable).Type {
let commands = programmableRobot.collectCommands()
print("Commands: \(commands)")
}
}
for type in allRobotTypes {
buildCommandsFor(type)
}