Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get rawValue from enum in a generic function

Update 8/28/2015: This will be solved in Swift 2

See Twitter response from Swift compiler developer

Update 10/23/2015: With Swift 2 generics you still cannot get the rawValue. You do can get the associated value.

Original question:

I have some generic reflection code written in swift. In that code I'm having trouble getting the value for properties that are based on an enum. The problem comes down to the fact that I'm not able to execute the .rawValue on the property value which is of type Any. The Swift reflection code will return the enum's value as type Any. So how can I get from an Any to an AnyObject which is the rawValue of the enum.

The only workaround i found so far is extending all enum's with a protocol. Below you can see a unit test that's OK using this workaround.

Is there any way to solve this without adding code to the original enum's?

for my reflection code I need the getRawValue method signature to stay as it is.

class WorkaroundsTests: XCTestCase {
    func testEnumToRaw() {
        let test1 = getRawValue(MyEnumOne.OK)
        XCTAssertTrue(test1 == "OK", "Could nog get the rawvalue using a generic function")
        let test2 = getRawValue(MyEnumTwo.OK)
        XCTAssertTrue(test2 == "1", "Could nog get the rawvalue using a generic function")
        let test3 = getRawValue(MyEnumThree.OK)
        XCTAssertTrue(test3 == "1", "Could nog get the rawvalue using a generic function")
    }


    enum MyEnumOne: String, EVRawString {
        case NotOK = "NotOK"
        case OK = "OK"
    }

    enum MyEnumTwo: Int, EVRawInt {
        case NotOK = 0
        case OK = 1
    }

    enum MyEnumThree: Int64, EVRaw {
        case NotOK = 0
        case OK = 1
        var anyRawValue: AnyObject { get { return String(self.rawValue) }}
    }

    func getRawValue(theEnum: Any) -> String {
        // What can we get using reflection:
        let mirror = reflect(theEnum)
        if mirror.disposition == .Aggregate {
            print("Disposition is .Aggregate\n")

            // OK, and now?

            // Thees do not complile:
            //return enumRawValue(rawValue: theEnum)
            //return enumRawValue2(theEnum )

            if let value = theEnum as? EVRawString {
                return value.rawValue
            }
            if let value = theEnum as? EVRawInt {
                return String(value.rawValue)
            }
        }
        var valueType:Any.Type = mirror.valueType
        print("valueType = \(valueType)\n")
        // No help from these:
        //var value = mirror.value  --> is just theEnum itself
        //var objectIdentifier = mirror.objectIdentifier   --> nil
        //var count = mirror.count   --> 0
        //var summary:String = mirror.summary     --> "(Enum Value)"
        //var quickLookObject = mirror.quickLookObject --> nil

        let toString:String = "\(theEnum)"
        print("\(toString)\n")
        return toString
    }

    func enumRawValue<E: RawRepresentable>(rawValue: E.RawValue) -> String {
        let value = E(rawValue: rawValue)?.rawValue
        return "\(value)"
    }

    func enumRawValue2<T:RawRepresentable>(rawValue: T) -> String {
        return "\(rawValue.rawValue)"
    }

}

    public protocol EVRawInt {
        var rawValue: Int { get }
    }
    public protocol EVRawString {
        var rawValue: String { get }
    }
    public protocol EVRaw {
        var anyRawValue: AnyObject { get }
    }
like image 317
Edwin Vermeer Avatar asked Jul 24 '15 10:07

Edwin Vermeer


People also ask

Can enum conform to Swift protocol?

Yes, enums can conform protocols. You can use Swift's own protocols or custom protocols. By using protocols with Enums you can add more capabilities.

What is raw value in enum Swift?

Enum raw values To set a value to your enum, you need to assign a data type to it. In our case above, we are using a type of String . Each raw value for our enum case must be a unique string, character, or value of any integer or floating-point type. This means the value for the two case statements cannot be the same.

What is enum or enumerations in Swift?

In Swift, an enum (short for enumeration) is a user-defined data type that has a fixed set of related values. We use the enum keyword to create an enum. For example, enum Season { case spring, summer, autumn, winter } Here, Season - name of the enum.

Why use enum in Swift?

Enums solve this problem by letting you define a new data type, then define the possible values it can hold. For example, we might say there are five kinds of weather: sun, cloud, rain, wind and snow. If we make this an enum, it means Swift will accept only those five values – anything else will trigger an error.


1 Answers

Unfortunately, this doesn't look like it's possible in Swift at this point, but I've thought about your problem for a while, and I'll propose 3 ways that the Swift team could enable you to solve this problem.

  1. Fix the mirror for enums. The most straightforward solution is one that I'm sure you already tried. You're trying to build a reflection library, and you'd like to reflect an Any value to see if it's an enum, and if it is, you'd like to see if it has a raw value. The rawValue property should be accessible via this code:

    let mirror = reflect(theEnum) // theEnum is of Any type
    for i in 0..<mirror.count {
        if mirror[i].0 == "rawValue" {
            switch mirror[i].1.value {
            case let s as String:
                return s
            case let csc as CustomStringConvertible:
                return csc.description
            default:
                return nil
            }
        }
    }
    

However, this doesn't work. You'll find that the mirror has a count of 0. I really think that this is an oversight on the part of the Swift team in their implementation of Swift._EnumMirror, and I'll be filing a radar about this. rawValue is definitely a legitimate property. It is a weird scenario because enums aren't allowed to have other stored properties. Also, your enum's declaration never explicitly conforms to RawRepresentable, nor does it declare the rawValue property. The compiler just infers that when you type enum MyEnum: String or : Int or whatever. In my tests, it appears that it shouldn't matter whether the property is defined in a protocol or is an instance of an associated type, it should still be a property represented in the mirror.

  1. Allow for protocol types with defined associated types. As I mentioned in my comment above, it's a limitation in Swift that you cannot cast to a protocol type that has associated type requirements. You can't simply cast to RawRepresentable because Swift doesn't know what type the rawValue property will return. Syntax such as RawRepresentable<where RawValue == String> is conceivable, or perhaps protocol<RawRepresentable where RawValue == String>. If this were possible, you could try to cast to the type through a switch statement as in:

    switch theEnum {
    case let rawEnum as protocol<RawRepresentable where RawValue == String>:
       return rawEnum.rawValue
    // And so on
    }
    

But that's not defined in Swift. And if you just try to cast to RawRepresentable, the Swift compiler tells you that you can only use this in a generic function, but as I look at your code, that's only led you down a rabbit-hole. Generic functions need type information at compile-time in order to work, and that's exactly what you don't have working with Any instances.

The Swift team could change protocols to be more like generic classes and structs. For example MyGenericStruct<MyType> and MyGenericClass<MyType> are legitimately specialized concrete types that you can make a runtime check on and cast to. However, the Swift team may have good reasons for not wanting to do this with protocols. Specialized versions of protocols (i.e. protocol references with known associated types) still wouldn't be concrete types. I wouldn't hold my breath for this ability. I consider this the weakest of my proposed solutions.

  1. Extend protocols to conform to protocols. I really thought I could make this solution work for you, but alas no. Since Swift's built-in mirror for enums doesn't deliver on the rawValue, I thought why not implement my own generic mirror:

    struct RawRepresentableMirror<T: RawRepresentable>: MirrorType {
        private let realValue: T
    
        init(_ value: T) {
            realValue = value
        }    
    
        var value: Any { return realValue }
        var valueType: Any.Type { return T.self }
        var objectIdentifier: ObjectIdentifier? { return nil }
        var disposition: MirrorDisposition { return .Enum }
        var count: Int { return 1 }
    
        subscript(index: Int) -> (String, MirrorType) {
            switch index {
            case 0:
                return ("rawValue", reflect(realValue.rawValue))
            default:
                fatalError("Index out of range")
            }
        }
    
        var summary: String {
            return "Raw Representable Enum: \(realValue)"
        }
    
        var quickLookObject: QuickLookObject? {
            return QuickLookObject.Text(summary)
        }
    }
    

Great! Now all we have to do is extend RawRepresentable to be Reflectable so that we can first cast theEnum as Reflectable (no associated type required) and then call reflect(theEnum) to give us our awesome custom mirror:

    extension RawRepresentable: Reflectable {
        func getMirror() -> MirrorType {
            return RawRepresentableMirror(self)
        }
    }

Compiler error: Extension of protocol 'RawRepresentable' cannot have an inheritance clause

Whaaaat?! We can extend concrete types to implement new protocols, such as:

    extension MyClass: MyProtocol {
        // Conform to new protocol
    }

As of Swift 2, we can extend protocols to give concrete implementations of functions, such as:

    extension MyProtocol {
        // Default implementations for MyProtocol
    }

I thought for sure we could extend protocols to implement other protocols, but apparently not! I see no reason why we couldn't have Swift do this. I think this would very much fit in with the protocol-oriented programming paradigm that was the talk of WWDC 2015. Concrete types that implement the original protocol would get a new protocol conformance for free, but the concrete type is also free to define its own versions of the new protocol's methods. I'll definitely be filing an enhancement request for this because I think it could be a powerful feature. In fact, I think it could be very useful in your reflection library.

Edit: Thinking back on my dissatisfaction with answer 2, I think there's a more elegant and realistic possibility for working with these protocols broadly, and that actually combines my idea from answer 3 about extending protocols to conform to new protocols. The idea is to have protocols with associated types conforming to new protocols that retrieve type-erased properties of the original. Here is an example:

protocol AnyRawRepresentable {
    var anyRawValue: Any { get }
}

extension RawRepresentable: AnyRawRepresentable {
    var anyRawValue: Any {
        return rawValue
    }
}

Extending the protocol in this way wouldn't be extending inheritance per se. Rather it would be just saying to the compiler "Wherever there is a type that conforms to RawRepresentable, make that type also conform to AnyRawRepresentable with this default implementation." AnyRawRepresentable wouldn't have associated type requirements, but can still retrieve rawValue as an Any. In our code, then:

if let anyRawEnum = theEnum as? AnyRawRepresentable {  // Able to cast to
    let anyRawValue = anyRawEnum.anyRawValue  // anyRawValue is of type Any
    switch anyRawValue {
    case let s as String:
        return s
    case let csc as CustomStringConvertible:
        return csc.description
    default:
        return nil
    }
}

This kind of solution could be used broadly with any kind of protocol with associated types. I will likewise be including this idea in my proposal to the Swift team for extending protocols with protocol conformance.

Update: None of the above options are available as of Swift 4. I did not receive a response as to why the Mirror on a RawRepresentable enum does not contain its rawValue. As for options #2 and #3, they are still within the realm of possibility for future releases of Swift. They have been mentioned on the Swift mailing list and in the Generics Manifesto document authored by Doug Gregor of the Swift team.

The proper term for option #2 ("Allow for protocol types with defined associated types") is generalized existentials. This would allow protocols with associated types to possibly automatically return Any where there is an associated type or allow for syntax like the following:

anyEnum as? Any<RawRepresentable where .RawValue == String>

Option #3 has been mentioned occasionally on the mailing list, and it is a commonly rejected requested feature, but that is not to say it couldn't be included in future versions of Swift. In the Generics Manifesto, it is termed "Conditional conformances via protocol extensions". While recognizing its power as a feature, it sadly also states that efficient implementation is "nearly impossible."

like image 70
Christopher Whidden Avatar answered Sep 20 '22 22:09

Christopher Whidden