Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to simplify almost equal enum extensions in Swift

I have extensions for about 20 enums which look like this:

extension CurrencyValue : JSONDecodable {
    static func create(rawValue: String) -> CurrencyValue {
        if let value = CurrencyValue(rawValue: rawValue) {
            return value
        }
        return .unknown
    }

    static func decode(j: JSONValue) -> CurrencyValue? {
        return CurrencyValue.create <^> j.value()
    }
}

extension StatusValue : JSONDecodable {
    static func create(rawValue: String) -> StatusValue {
        if let value = StatusValue(rawValue: rawValue) {
            return value
        }
        return .unknown
    }

    static func decode(j: JSONValue) -> StatusValue? {
        return StatusValue.create <^> j.value()
    }
}

They are almost the same except the enum type name and I have 20 of them - thats obviously very stupid. Does anybody have an idea how to reduce them to one, maybe by using generics? I have no idea at the moment.

UPDATE

The enums are as simple as this:

enum CurrencyValue : String {
    case EUR = "EUR"
    case unknown = "unknown"
}

enum StatusValue : String {
    case ok = "ok"
    case pending = "pending"
    case error = "error"
    case unknown = "unknown"
}

And lets assume the following:

  1. Every ENUM has the .unknown case
  2. I need to exchange the enum type in the extension with something generic.

There must be some trick to not implement the same extension multiple times and just alter the type.

UPDATE

As it s stated by Gregory Higley below I use the JSON lib Argo. You can read about the operators there.

like image 292
blackjacx Avatar asked Jan 08 '15 21:01

blackjacx


People also ask

Can we extend enum in Swift?

A Swift extension allows you to add functionality to a type, a class, a struct, an enum, or a protocol. But extensions are more powerful than that.

Can Swift enums have methods?

Swift's Enum can have methods. It can have instance methods and you can use it to return expression value for the UI. Let's look at the code above.

Is enum value type in Swift?

In Swift there are two categories of types: value types and reference types. A value type instance keeps a unique copy of its data, for example, a struct or an enum . A reference type, shares a single copy of its data, and the type is usually a class .

What is associated enum?

An enum cannot have both raw values and associated values at the same time. The raw values of an enum must be of the same data type. But associated values can be of any type.


1 Answers

The essence of your question is that you want to avoid writing boilerplate code for all of these enumerations when implementing Argo's JSONDecodable. It looks like you've also added a create method, which is not part of the type signature of JSONDecodable:

public protocol JSONDecodable {
  typealias DecodedType = Self
  class func decode(JSONValue) -> DecodedType?
}

Unfortunately this cannot be done. Swift protocols are not mixins. With the exception of operators, they cannot contain any code. (I really hope this gets "fixed" in a future update of Swift. Overridable default implementations for protocols would be amazing.)

You can of course simplify your implementation in a couple of ways:

  1. As Tony DiPasquale suggested, get rid of .unknown and use optionals. (Also, you should have called this .Unknown. The convention for Swift enumeration values is to start them with a capital letter. Proof? Look at every enumeration Apple has done. I can't find a single example where they start with a lower case letter.)
  2. By using optionals, your create is now just a functional alias for init? and can be implemented very simply.
  3. As Tony suggested, create a global generic function to handle decode. What he did not suggest, though he may have assumed it was implied, was to use this to implement JSONDecodable.decode.
  4. As a meta-suggestion, use Xcode's Code Snippets functionality to create a snippet to do this. Should be very quick.

At the asker's request, here's a quick implementation from a playground. I've never used Argo. In fact, I'd never heard of it until I saw this question. I answered this question simply by applying what I know about Swift to an examination of Argo's source and reasoning it out. This code is copied directly from a playground. It does not use Argo, but it uses a reasonable facsimile of the relevant parts. Ultimately, this question is not about Argo. It is about Swift's type system, and everything in the code below validly answers the question and proves that it is workable:

enum JSONValue {
    case JSONString(String)
}

protocol JSONDecodable {
    typealias DecodedType = Self
    class func decode(JSONValue) -> DecodedType?
}

protocol RawStringInitializable {
    init?(rawValue: String)
}

enum StatusValue: String, RawStringInitializable, JSONDecodable {
    case Ok = "ok"
    case Pending = "pending"
    case Error = "error"

    static func decode(j: JSONValue) -> StatusValue? {
        return decodeJSON(j)
    }
}

func decodeJSON<E: RawStringInitializable>(j: JSONValue) -> E? {
    // You can replace this with some fancy Argo operators,
    // but the effect is the same.
    switch j {
    case .JSONString(let string): return E(rawValue: string)
    default: return nil
    }
}

let j = JSONValue.JSONString("ok")
let statusValue = StatusValue.decode(j)

This is not pseudocode. It's copied directly from a working Xcode playground.

If you create the protocol RawStringInitializable and have all your enumerations implement it, you will be golden. Since your enumerations all have associated String raw values, they implicitly implement this interface anyway. You just have to make the declaration. The decodeJSON global function uses this protocol to treat all of your enumerations polymorphically.

like image 138
Gregory Higley Avatar answered Oct 06 '22 03:10

Gregory Higley