swiftenumscoding-stylefirst-class-functions

Swift: Calling an Enum value converter function as a first class function


enum SolarSystemPlanet: String, CaseIterable {
    case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune

    func toRawValue(_ value: SolarSystemPlanet) -> PlanetName {
        value.rawValue
    }
}

With the enum above, one way to get an array of planet names is to call

SolarSystemPlanet.allCases.map { $0.rawValue }

But Swift supports first-class functions, treating functions as "first class citizens", which allows us to call functions just like any other object or value.

So it would be nice to get an array of names by this

SolarSystemPlanet.allCases.map(.toRawValue)

However, it seems like the compiler needs more context. It couldn't infer a type in map at compile-time, so I did

SolarSystemPlanet.allCases.map(SolarSystemPlanet.toRawValue)

The compiler stops complaining, but I'm not getting an array of String. The line above returns a value of type [(SolarSystemPlanet) -> String]

If I printed the above out, instead of getting

["mercury", "venus", "earth", "mars", "jupiter", "saturn", "uranus", "neptune"]

I got

[(Function), (Function), (Function), (Function), (Function), (Function), (Function), (Function)]

If I forced the return type to be [String] like this

var planets: [String] = SolarSystemPlanet.allCases.map(SolarSystemPlanet.toRawValue)

Xcode would complain that [(SolarSystemPlanet) -> String] can't be converted to [String]

Is it possible after all to achieve what I'm trying to do? Am I missing something or doing something wrong?

And if it's not possible, I would also really appreciate some explanations on why.

Thanks for spending time reading my question!


Edit Thanks for @sweeper's answer.

For those who are interested, I went slightly further to make sure every String enum has toRawValue

extension RawRepresentable where RawValue == String {
    static func toRawValue(_ value: Self) -> PlanetName {
        value.rawValue
    }
}

Note: This is Swift 5.1.3


Solution

  • Note that toRawValue doesn't need to be an instance method. It can be static:

    static func toRawValue(_ value: SolarSystemPlanet) -> PlanetName {
        value.rawValue
    }
    

    Now you can use SolarSystemPlanet.toRawValue as the argument to map.

    Alternatively, in this case, you can also use the keypath of \.rawValue as the argument to map:

    SolarSystemPlanet.allCases.map(\.rawValue)
    

    This is a new feature of Swift 5.2.

    EDIT: Explanation of why instance methods don't work

    In Swift, when accessed from a static context, an instance method on a type T that has the signature (U) -> R becomes a static method with the signature (T) -> ((U) -> R). Instance methods need an instance of the enclosing type to call, right? So when you pass it a T, it gives you the original instance function (U) -> R back.

    Therefore, the type of the non-static SolarSystemPlanet.toRawValue is

    (SolarSystemPlanet) -> ((SolarSystemPlanet) -> String)
    

    This explains why after map is applied, the array becomes a [(SolarSystemPlanet) -> String].