Type Safety in Swift: Enums with Associated Values (Updated)


Following my first post on Type Safety in Swift, I was asked (by @DeFrenz) why I hadn't used enums (or an Either type). It was a question that made me think again about the employment of enums in the various JSON parsing libraries that have been released for Swift, and to look again at Apple's documentation for enumerations, and for those employing associated values in particular.

This was a useful exercise because it took me back to the real issue of dealing with AnyObject or NSObject dictionaries and arrays (which is one misalignment with a strongly typed language such as Swift) and the need to take a value and transform it into one of two or more types.

Individually wrapped

The main difference between the approach to type safety that I took in the previous two posts on type safety is that, with the use of enums, individual values can be wrapped rather than relying on division into multiple dictionaries and arrays.

Starting with the enum

The starting point for this approach is the enum.
enum TypeChoices {
    case StringType(String)
    case IntegerType(Int)
    case StringArray([String])
    case IntegerArray([Int])

}
Here the various types are set out. These will be available within the dictionary that we are going to create.

(For reference see 'The Power of Swift' by Chris Eidhof.)

Wrapping up

For ease, and to prevent lots of inline switch statements, a struct is now created to leverage the enum:
struct Value {
    private let typeChoice:TypeChoices
    
    init(_ val:String) {
        typeChoice = TypeChoices.StringType(val)
    }
    init(_ val:[String]) {
        typeChoice = TypeChoices.StringArray(val)
    }

    init(_ val:Int) {
        typeChoice = TypeChoices.IntegerType(val)
    }
    init(_ val:[Int]) {
        typeChoice = TypeChoices.IntegerArray(val)
    }
}

Un-wrapping

So now we've got a way to initialise a value and assign enum cases, we need a way to get at that value by extending the Value type:
extension Value {
    var str:String? {
        switch typeChoice {
        case .StringType(let str):
            return str
        default:
            return nil
        }
        
    }
    var strArr:[String]? {
        switch typeChoice {
        case .StringArray(let strArr):
            return strArr
        default:
            return nil
        }
        
    }
    var num:Int? {
        switch typeChoice {
        case .IntegerType(let num):
            return num
        default:
            return nil
        }
        
    }
    var numArr:[Int]? {
        switch typeChoice {
        case .IntegerArray(let numArr):
            return numArr
        default:
            return nil
        }
        
    }

}

Taking a test drive

Finally we're ready for some code to see what this all does in practice.
let mixedValueArray = ["one":Value(["a","b","c"]),"two":Value("Hi World"),"three":Value([1,2,3,4,5]),"four":Value("Hi World!")]

mixedValueArray["four"]?.str // "Hi World!"

if let arr = mixedValueArray["one"]?.strArr {
    for v in arr {
        println(v)
    }
}

Automating the initialisation

It's cumbersome to be typing Value() all the time and in real world use it's not going to help a great deal when a dictionary containing AnyObject or NSObject values is received. So in order to deal with this situation the Value type needs to be further extended:
extension Value {
    
    init? (_ val:AnyObject) {
        if let v = val as? String {
            typeChoice = TypeChoices.StringType(v)
        }
        else if let v = val as? [String] {
            typeChoice = TypeChoices.StringArray(v)
        }
        else if let v = val as? Int {
            typeChoice = TypeChoices.IntegerType(v)
        }
        else if let v = val as? [Int] {
            typeChoice = TypeChoices.IntegerArray(v)
        }
        else {
            return nil
        }
    }

}
You'll notice that the initialiser that has been added is a failable initialiser. This means that if the value cannot be cast to an expected type we'll know about it and can take action accordingly.

The final extension

Nearly there, but we need now to add a method that can do the conversion work.
extension Value {
    static func mixedValueDictionary(arr:[String:AnyObject]) -> [String:Value] {
        var mixedDictionary = [String:Value]()
        for (k,v) in arr {
            if let val = Value(v) {
                mixedDictionary[k] = val
            }
        }
        return mixedDictionary
    }
}
The code loops through the supplied dictionary and builds a new dictionary of type [String:Value] from the supplied one.

Finally we're ready to test it out:
let dict:[String:AnyObject] = ["one":["a","b","c"],"two":"Hi World","three":[1,2,3,4,5],"four":["one":1]]

let mixDict = Value.mixedValueDictionary(dict)
mixDict["two"]?.str // "Hi World!"

Conclusion

There's a fair amount of code here, but really what's happening is fairly simple to summarise: each value in a dictionary is wrapped up and when it comes to unwrapping, if the correct and expected types are the same it can be unwrapped. In this way the whole process maintains type safety.

The next step will be to enable the alteration of values and to return a dictionary in the same form that it was received.

Update: Outside the Comfort Zone

This post began with a twitter response and it ends with a twitter response. Once I'd sent this post out into the world I received a tweet from @clattner_llvm. He rightly pointed out that I didn't need a struct at all and that everything could happen within the enum.

So here's the code reworked to use a single enum type:
enum Value {
    case StringType(String)
    case IntegerType(Int)
    case StringArray([String])
    case IntegerArray([Int])
}

extension Value {
    var str:String? {
        switch self {
        case .StringType(let str):
            return str
        default:
            return nil
        }
        
    }
    var strArr:[String]? {
        switch self {
        case .StringArray(let strArr):
            return strArr
        default:
            return nil
        }
        
    }
    var num:Int? {
        switch self {
        case .IntegerType(let num):
            return num
        default:
            return nil
        }
        
    }
    var numArr:[Int]? {
        switch self {
        case .IntegerArray(let numArr):
            return numArr
        default:
            return nil
        }
        
    }
}

extension Value {
    
    init(_ val:String) {
        self = .StringType(val)
    }
    init(_ val:[String]) {
        self = .StringArray(val)
    }
    
    init(_ val:Int) {
        self = .IntegerType(val)
    }
    init(_ val:[Int]) {
        self = .IntegerArray(val)
    }
    
}

extension Value {
    
    init? (_ val:AnyObject) {
        if let v = val as? String {
            self = .StringType(v)
        }
        else if let v = val as? [String] {
            self = .StringArray(v)
        }
        else if let v = val as? Int {
            self = .IntegerType(v)
        }
        else if let v = val as? [Int] {
            self = .IntegerArray(v)
        }
        else {
            return nil
        }
    }
    
    
}

extension Value {
    static func mixedValueDictionary(arr:[String:AnyObject]) -> [String:Value] {
        var mixedDictionary = [String:Value]()
        for (k,v) in arr {
            if let val = Value(v) {
                mixedDictionary[k] = val
            }
        }
        return mixedDictionary
    }
}
var aValue = Value(6)
if let abcde = aValue?.num {
    abcde // 6
}

let dict:[String:AnyObject] = ["one":["a","b","c"],"two":"Hi World","three":[1,2,3,4,5],"four":["one":1]]

let mixDict = Value.mixedValueDictionary(dict)
mixDict["two"]?.str // "Hi World!"
I'll now be using this code as the starting point from which to begin the next post.



Endorse on Coderwall

Comments