1

I have a case where I am trying to define a function that takes in an array of objects with the requirement that each object must define a string-based enum called 'Commands'.

Here's an example of how you would do it if you were using an associated type:

protocol CommandSetProtocol {

    associatedtype Command : RawRepresentable where Command.RawValue == String

    var commands:[Command] { get }
}

class FooCommands : CommandSetProtocol {

    enum Command : String {
        case commandA = "Command A"
        case commandB = "Command B"
    }

    let commands = [
        Command.commandA,
        Command.commandB
    ]
}

class LaaCommands : CommandSetProtocol {

    enum Command : String {
        case commandC = "Command C"
        case commandD = "Command D"
    }

    let commands = [
        Command.commandC,
        Command.commandD
    ]
}

The problem is you can't do this because of that associated type:

var commandSets:[CommandSetProtocol.Type] = [
    FooCommands.self,
    LaaCommands.self
]

Of note: I'm trying to stop someone form doing this, which should fail compilation because the Raw type is not a string.

class BadCommands : CommandSetProtocol {

    enum Command : Int {
        case commandE = 1
        case commandF = 2
    }

    let commands = [
        Command.commandE,
        Command.commandF
    ]
}

How can this (or similar) be achieved?

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • You need a generic here. You are currently saying that the adopter must type `someStringRawRepresentable` as RawRepresentable, which is not what you mean. You mean that the adopter must type `someStringRawRepresentable` as some RawRepresentable _adopter_. As soon as you make this is a generic, the answer will fall into your lap. – matt Feb 06 '18 at 19:42
  • 1
    "normally I'd use an associated type, but for design reasons, that's not possible here" Well then you're up a creek, because that's how you do it (make this a generic). – matt Feb 06 '18 at 19:43
  • Not quite. Adding more code to explain the problem. – Mark A. Donohoe Feb 06 '18 at 19:46
  • So, everything you want does happen correctly (LaaCommands and FooCommands compile, BadCommands doesn't) as long as you omit the part about the `commandSets`. And that's because you're doing exactly what I said: you've made CommandSetProtocol a generic. So it seems to me that the question is not about that at all; it's about `commandSets`. You're trying to make an array of metatypes; Swift doesn't do that. The entire question, as you've posed it, is a complete red herring! – matt Feb 06 '18 at 19:56
  • 1
    Of course, you are also hitting the "can only be used as a generic constraint" wall. The way around that is type erasure. But you're not going to be able to do that here, because, as I said, Swift doesn't do arrays of metatypes no matter _how_ you come at it. – matt Feb 06 '18 at 20:02
  • I've updated the question yet again to satisfy your 'red herring' comment. – Mark A. Donohoe Feb 06 '18 at 20:03
  • Also, like I said in the last sentence, you understand what I'm trying to do even if this isn't the way to do it. As such, how would you go about it? How would you use protocols to enforce an object exposes a RawRepresentable:String type which you can then stuff in an array? – Mark A. Donohoe Feb 06 '18 at 20:05
  • 2
    You've expressed it beautifully, but, as I say, type erasure is the way forward when you hit the "can only be used as a generic constraint" wall. – matt Feb 06 '18 at 20:05
  • Look above. I'm creating an array of types which meet the requirement I'm specifying (i.e. [CommandSetProtocol.Type]) The CommandSetProtocol-adhering type is what I want in the array. Not an instance of that type. – Mark A. Donohoe Feb 06 '18 at 20:07
  • And to explain why I need the array of types, not instances, it's because the 'commandSets' variable is then used to build up metadata which is then fed to the Xcode extension. It's a type-building system that I'm trying to simplify how we define the specific commands. I could post more, but it would confuse the point of the question. – Mark A. Donohoe Feb 06 '18 at 20:11
  • 1
    I also use arrays of types all the time. Very common tool. I have a system right now that detects what kind of hardware is connected, and it does that by attempting to construct each of a series of types which have an `init?(...)` method. First one that returns an object wins. The list of supported types is stored in an array, making it easy to add or remove supported types (including mock types in testing). – Rob Napier Feb 06 '18 at 20:18
  • 1
    But your example, BTW, actually captures a deep reason this isn't possible. What would you do with `commandSets[0]`? That's a type that conforms to `CommandSetProtocol`, but you don't know which enum. So you could call `init?(string:)`, but what could you possibly do with the result? Try to write the switch statement you'd want to use. It won't work, because you don't know the actual type so what are the cases? – Rob Napier Feb 06 '18 at 20:28
  • 1
    I often find the secret to these problems is to start with the code I want to write to *use* the type (without using `as?` anywhere), and then work backwards to what kind of type that should be. – Rob Napier Feb 06 '18 at 20:29
  • That's an approach I often use myself! :) – Mark A. Donohoe Feb 06 '18 at 21:11

2 Answers2

1

Eliminating all the red herrings in the question, you are really just pointing out the well-known fact that this sort of thing is legal:

protocol P {}
class A:P {}
class B:P {}

let arr : [P] = [A(), B()]

... but this sort of thing is not:

protocol P {associatedtype Assoc}
class A:P {typealias Assoc=String}
class B:P {typealias Assoc=String}

let arr : [P] = [A(), B()]

The problem is that you hit the "can only be used as a generic constraint" wall. That wall is due to be torn down in a future version of Swift, but until then, the way to make that array is to use type erasure.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    But that's why the very subject of the question, as well as the text specifically asks how you would achieve this *without* using the associated type. Guess the answer is 'you can't'. – Mark A. Donohoe Feb 06 '18 at 20:13
  • Yes, as I said in my first comment (and my second) up front. What was the _first thing I said_? "You need a generic here." That's the associated type. – matt Feb 06 '18 at 20:23
0

Ok, I figured out how to get what I needed.

As you know, the issue is I needed to restrict the type of the RawRepresentable, which requires a generic/associated type, but if you use that, then you can't define a variable as an array of that type.

But then I asked myself why did I need that. The answer is because I'm using that RawRepresentable:String to build up another collection of CommandDefinition objects, and it's that value that I'm really interested in. So, the solution was to use a second level of protocols with that second level having the associated type to satisfy the requirements of the first level (base) protocol which can't have them.

Here's the above re-written, with the missing pieces of the puzzle added.

First, the reusable framework that can be added to any extension project as-is. Its comprised of the CommandSetBase, CommandSet, ExtensionBase and an extension on CommandSet:

typealias CommandDefinition = [XCSourceEditorCommandDefinitionKey: Any]

protocol CommandSetBase : XCSourceEditorCommand {

    static var commandDefinitions : [CommandDefinition] { get }
}

protocol CommandSet : CommandSetBase {

    associatedtype Command : RawRepresentable where Command.RawValue == String

    static var commands:[Command] { get }
}

class ExtensionBase : NSObject, XCSourceEditorExtension {

    var commandSets:[CommandSetBase.Type]{
        return []
    }

    final var commandDefinitions: [CommandDefinition] {
        return commandSets.flatMap{ commandSet in commandSet.commandDefinitions }
    }
}

Here's the extension for CommandSet that uses the CommandSet-defined 'commands' associated type to satisfy the CommandSetBase requirement of the commandDefinitions (this was the missing piece):

extension CommandSet {

    static var commandDefinitions:[CommandDefinition] {

        return commands.map({

            command in

            return [
                XCSourceEditorCommandDefinitionKey.classNameKey  : String(reflecting:self),
                XCSourceEditorCommandDefinitionKey.identifierKey : String(describing:command),
                XCSourceEditorCommandDefinitionKey.nameKey       : command.rawValue
            ]
        })
    }
}

And here's the app-specific implementation of the command sets and the extension that uses them.

First, the extension itself...

class Extension : ExtensionBase {

    override var commandSets:[CommandSetBase.Type]{

        return [
            NavigationCommands.self,
            SelectionCommands.self
        ]
    }

    func extensionDidFinishLaunching() {

    }
}

Now the Selection commands:

class SelectionCommands: NSObject, CommandSet {

    enum Command : String {
        case align            = "Align"
        case alignWithOptions = "Align with options..."
    }

    static let commands = [
        Command.align,
        Command.alignWithOptions
    ]

    func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {

        print("You executed the Selection command \(invocation.commandIdentifier)")

        completionHandler(nil)
    }
}

And lastly, the Navigation commands:

class NavigationCommands : NSObject, CommandSet {

    enum Command : String {
        case jumpTo = "Jump to..."
        case goBack = "Go back"
    }

    static let commands = [
        Command.jumpTo,
        Command.goBack
    ]

    func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {

        print("You executed the Navigation command \(invocation.commandIdentifier)")

        completionHandler(nil)
    }
}

And here's the result...

enter image description here

If Swift ever allows you to enumerate the cases of an enum, then I could eliminate the seemingly-redundant 'static let commands' in the CommandSets above.

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286