Trying to define types for collaborators that are constrained to type of one side of the collaboration

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

Usually when compiler fails to infer generic types, one of the constraint for generic is missing.

This case fails because R is not guaranteed to be MetalMickey in that function.

static func defineCommands<R>(_ builder: CommandBuilder<R>) where R: Robot & Programmable {
    /// !!! Here's the problem
    if let dance = dance as? CommandBinding<R, DanceCommand> {
        builder.add(dance)
    }
}

If you look closely R can be other Robots, say WoodMickey, but dance is fixed to MetalMickey.

Yes. the question is how to solve this? The function requirement in Programmable needs to then constrain on Self, but that then prevents use of Programmable in “as?” And then we cannot call the defineCommands function on programmable robots.

On a side note, you don’t need to store command and robot in CommandBinding, R and C already contain all the information and you can just use them.

class CommandBinding<R, C> where R: Robot, C: Command {
    func displayText() -> String {
        return "\(C.name) on \(R.description)"
    }
}

let dance = CommandBinding<MetalMickey, DanceCommand>()
1 Like

Is there a scenario Programmable doesn’t imply Robot? How would defineCommands play out in those scenario?

If all the Programmable are Robot, you can likely do:

protocol Programmable: Robot {
    static func defineCommands(_ builder: CommandBuilder<Self>)
}