4

I'm trying to achieve something similar to how Swift utilizes the CodableKeys protocol set on an enumeration defined within a class that implements Codable. In my case, the class is CommandHandler and the enumeration is CommandIds and it doesn't require on code-gen from the compiler as the enum will always be explicitly specified.

Here's a simplified version of what I'm after...

protocol CommandId{}
protocol CommandHandler{
    associatedtype CommandIds : CommandId, RawRepresentable
}

class HandlerA : CommandHandler{
    enum CommandIds : String, CommandId{
        case commandA1
        case commandA2
    }
}

class HandlerB : CommandHandler{
    enum CommandIds : String, CommandId{
        case commandB1
        case commandB2
        case commandB3
    }
}

func processHandler<T:CommandHandler>(_ handler:T){
    // Logic to iterate over CommandIds. <-- This is where I get stumped
}

let handlerA = HandlerA()
processHandler(handlerA)

I'm struggling with the code inside processHandler here because I'm not sure how to reach the enumeration's values from a handler instance.

So what am I missing? What would be the code to get the values of the associated enumeration?

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

5 Answers5

1

Ok, I believe I have all of the pieces in place to show how you can do this in Swift. Turns out my revised question was right at the edge of being correct in how to do it.

Here's my example written in Swift 4...

First, here's how you define the protocols needed to make this work. From the design standpoint, these are synonymous with CodableKeys and Codable respectively.

protocol CommandId : EnumerableEnum, RawRepresentable {}

protocol CommandHandler{
    associatedtype CommandIds : CommandId
}

Here's a protocol and its associated extension to make the 'case' values of enums enumerable. You simply make your enums adhere to the EnumerableEnum protocol and you get a 'values' array.

Since the CommandId protocol above will already be applied to the enums in question, we simplify things by making it also apply the EnumerableEnum protocol in its own definition. This way we only need to apply CommandId to our enums and we get both.

public protocol EnumerableEnum : Hashable {
    static var values: [Self] { get }
}

public extension EnumerableEnum {

    public static var values: [Self] {

        let valuesSequence = AnySequence { () -> AnyIterator<Self> in

            var caseIndex = 0

            return AnyIterator {
                let currentCase: Self = withUnsafePointer(to: &caseIndex){
                    $0.withMemoryRebound(to: self, capacity: 1){
                        $0.pointee
                    }
                }
                guard currentCase.hashValue == caseIndex else {
                    return nil
                }
                caseIndex += 1
                return currentCase
            }
        }

        return Array(valuesSequence)
    }
}

Here are two classes that implement my CommandHandler/CommandId protocols

class HandlerA : CommandHandler{

    enum CommandIds : Int, CommandId{
        case commandA1
        case commandA2
    }
}

class HandlerB : CommandHandler{
    enum CommandIds : String, CommandId{
        case commandB1 = "Command B1"
        case commandB2
        case commandB3 = "Yet another command"
    }
}

Here's a test function which accepts a CommandHandler type

func enumerateCommandIds<T:CommandHandler>(_ commandHandlerType:T.Type){

    for value in commandHandlerType.CommandIds.values{
        let caseName     = String(describing:value)
        let caseRawValue = value.rawValue

        print("\(caseName) = '\(caseRawValue)'")
    }
}

And finally, here's the results of running that test

enumerateCommandIds(HandlerA.self)
// Outputs
//     commandA1 = '0'
//     commandA2 = '1'

enumerateCommandIds(HandlerB.self)
// Outputs
//     commandB1 = 'Command B1'
//     commandB2 = 'commandB2'
//     commandB3 = 'Yet another command'

It was a long, windy road to get here, but we did! Thanks to everyone for their help!

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

The only solution come up in my mind is using associatedtype and moving the enum outside the protocol, doing something like:

enum Commands:String {
    case default_command = ""
}

protocol CommandDef {
    associatedtype Commands
}

class MyClassA : CommandDef {
    enum Commands : String {
        case commandA1
        case commandA2 = "Explicit A2"
    }
}

class MyClassB : CommandDef {
    enum Commands : String {
        case commandB1
        case commandB2 = "Explicit B2"
        case commandB3
    }
}
mugx
  • 9,869
  • 3
  • 43
  • 55
  • But you don't have to (and actually, can't!) move the CodableKeys outside of the type you're defining as Codable. I've been trying to dig through the Swift source to see how they did it, but it's pretty complex in there. – Mark A. Donohoe Dec 13 '17 at 01:54
0

Having enums in protocols is not allowed in swift. If it were possible, the protocol would not be allowed to refer to the enumerated cases. You'd have to as cast eventually breaking protocol ideals.

Maybe associated types best serve your purpose?

enum Commands:String {
    case compliaceType = ""
}

protocol CommandDef {
    associatedtype Commands
}

class MyClassA : CommandDef {

    enum Commands : String {
        case commandA1 = "hi"
        case commandA2 = "Explicit A2"
    }


}

class MyClassB : CommandDef {
    enum Commands : String {
        case commandB2 = "Explicit B2"
    }

}

print(MyClassA.Commands.commandA1)
modesitt
  • 7,052
  • 2
  • 34
  • 64
0

There isn’t a way with the Swift language itself to mimic Codable, because Codable’s implementation relies on the compiler generating special-case code. Specifically, there is no protocol extension that creates the default CodingKeys enum, the compiler creates that enum inside a type that conforms to Codable automatically, unless you specify it yourself.

This is similar to how the Swift compiler will automatically create an initializer for structs (the “memberwise initializer”) unless you specify your own initializer. In that case as well, there is no protocol extension or Swift language feature you can use to replicate the auto-generated struct initializer, because it is based on metaprogramming / code generation, in this case, by the compiler.

There are tools, such as Sourcery (https://github.com/krzysztofzablocki/Sourcery), which allow you to implement your own metaprogramming and code generation. With Sourcery, you could run a script in your build phase that would automatically generate the code for the Command enum you want, and add it to any type that conforms toCommandHandler.

This would essentially mimic how Codable works via the Swift compiler generating needed code. But in neither case is it accomplished via Swift language features like protocol extensions, etc. Rather, it is boilerplate source code that gets written by a script rather than having to be written by hand.


UPDATE FOR REVISED QUESTION


If simply ensuring there is a way to enumerate all the cases of the CommandIds enum is all that you need, you can always add a protocol requirement to the CommandId protocol like this:

protocol CommandId {
    static var all: [Self] { get }
}

Then implementations would need to look like:

class HandlerA : CommandHandler {
    enum CommandIds : String, CommandId {
        case commandA1
        case commandA2

        static var all: [CommandIds] { return [.commandA1, .commandA2] }
    }
}

And your process function could look like:

func processHandler<T:CommandHandler>(_ handler:T){
    T.CommandIds.all.forEach { // Do something with each command case }
}

It's worth continuing to note though, that for Codable, Swift does not have or use any language functionality to enumerate all cases. Instead, the compiler uses knowledge of all the properties of the Codable-conforming type to generate a specific implementation of the init(from decoder: Decoder) for that type, including a line for each case, based on the known property names and types, e.g.

// This is all the code a developer had to write
struct Example: Codable {
  let name: String
  let number: Int
}

// This is all source code generated by the compiler using compiler reflection into the type's properties, including their names and types
extension Example {
  enum CodingKeys: String, CodingKey {
    case name, number
  }

  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    name = try values.decode(String.self, forKey: .name)
    number = try values.decode(Int.self, forKey: .number)
  }
}

Since Swift is a mostly static language with extremely limited runtime reflection (for now), there is no way to do these types of tasks at runtime using language features.

But there is nothing stopping you or any developer from using code generation the same way the Swift compiler does to accomplish similar conveniences. In fact, a well-known member of the Swift core team at Apple even encouraged me to do so when I presented him some challenges I was facing at WWDC.

It's also worth noting that features that are now part of the Swift compiler or have open pull requests to be added to the Swift compiler (like Codable, and automatic conformance to Equatable and Hashable) were first created and implemented in real-world Swift projects using Sourcery, before they were added to Swift itself.

Daniel Hall
  • 13,457
  • 4
  • 41
  • 37
  • I've revised/rewrote the question to be more clear/concise about what I'm after. – Mark A. Donohoe Dec 13 '17 at 07:11
  • @MarqueIV. Updated the answer, hope that helps. – Daniel Hall Dec 13 '17 at 16:18
  • Thanks for the update. However, that requires manual duplication of all keys into the array. That said, I've been trying something similar where a protocol extension on CommandId enumerates the values for you so you don't have to create the array manually. It's promising, but there's still a few kinks I'm trying to work out. More on that as I go... – Mark A. Donohoe Dec 13 '17 at 16:21
  • Yep, it does require manual duplication of all keys into the array. And as noted, this is why Swift needs to use compiler reflection and code generation to accomplish this, instead of languages features (since the language features don't currently exist to do so). Consider this answer again if you run out of ideas for trying to reflect into enums using Swift language features! Or, if you find a great solution please share it here so we know how you did it :) – Daniel Hall Dec 13 '17 at 16:39
  • I actually do have a Swift way to do this (i.e. enumerate over an enum's values) provided you make your enums adhere to a protocol, which is fine because `Codable` makes its enum adhere to `CodableKeys` and that's essentially the same thing we're doing here. More to come once I clean it up a little more. – Mark A. Donohoe Dec 13 '17 at 17:29
0

You can do this easily using Swift's CaseIterable protocol.

protocol CommandId: CaseIterable {
    func handle()
}

protocol CommandHandler {
    associatedtype CommandIds: CommandId, RawRepresentable
}

class HandlerA: CommandHandler {
    enum CommandIds: String, CommandId {
        case commandA1
        case commandA2

        func handle() {
            print("\(rawValue) is handled")
        }
    }
}

class HandlerB: CommandHandler {
    enum CommandIds: String, CommandId {
        case commandB1
        case commandB2
        case commandB3

        func handle() {
            print("\(rawValue) is handled")
        }
    }
}

func processHandler<T: CommandHandler>(_ handler: T) {
    // Logic to iterate over CommandIds. <-- This is where I get stumped
    T.CommandIds.allCases.forEach({ $0.handle() })
}

let handlerA = HandlerA()
processHandler(handlerA)
Alireza
  • 193
  • 2
  • 9