Literal Enumerations
Let’s say we want to dig a value out of pile of JSON. For example, what if we want to pull Bobbin’s name out of the following:
{
"characters":{
"LucasArts":[
{"name":"Guybrush Threepwood"},
{"name":"Bobbin Threadbare"},
{"name":"Manny Calavera"}
]
}
}
We might represent the path1 used to get to this data with an array like so:
["characters", "LucasArts", 1, "name"]
The problem is, JSON freely mixes dictionaries with arrays. So some of our path elements are String
s, while some are Int
indexes. Swift arrays are homogenous — that is, everything in them has to have the same type. We can’t mix-and-match String
s and Int
s in a type-safe Array
.
We’ve talked before about how Swift enumerations are sum types that can represent one of a number of different types. So we can create an enum
that represents both String
-based keys and Int
-based indexes, and use it as the type of our array:
enum JSONSubscript{
case Key(String), Index(Int)
}
let path:[JSONSubscript] = [.Key("characters"), .Key("LucasArts"), .Index(1), .Key("name")]
Which works and has a nice sort of DSL feel to it. But if we use more than a handful of enumerations in our code, it’s dangerously easy for us to be overwhelmed by a flood of .This("thing")
and .That("thing")
. Can we do better?
An oft-overlooked feature of Swift enumerations is that we can define initializers for them, just like class
es and struct
s:
enum JSONSubscript{
case Key(String), Index(Int)
init(value:String){
self = .Key(value)
}
init(value:Int){
self = .Index(value)
}
}
And if we can write initializers, we can conform to ...LiteralConvertible
protocols.
Mattt Thompson has written a fantastic explanation of literal convertibles that we should all read. But to summarize, a type that conforms to one of the literal convertible protocols can use the given literal to initialize itself.
To put it another way, we usually think of 5
as being a literal representation of an Int
. But if JSONSubscript
were to implement the IntegerLiteralConvertible
protocol, 5
could also be a literal representation of JSONSubscript
.
All it takes is a few init
s… and some typealias
es in the case of StringLiteralConvertible
:
enum JSONSubscript : IntegerLiteralConvertible, StringLiteralConvertible{
case Key(String), Index(Int)
//for IntegerLiteralConvertible:
init(integerLiteral value: IntegerLiteralType){
self = .Index(value)
}
//for StringLiteralConvertible
init(unicodeScalarLiteral value:String){
self = .Key(value)
}
init(extendedGraphemeClusterLiteral val:String){
self = .Key(val)
}
init(stringLiteral value:StringLiteralType){
self = .Key(value)
}
}
Now, I’ll be the first to concede the implementation of StringLiteralConvertible
is pretty verbose2…
[UPDATE: thanks to a little help from the message boards, the above is now much less long-winded. I hereby retract my claims of verbosity!]
…But in exchange, look what our path array has become:
let path:[JSONSubscript] = ["characters", "LucasArts", 1, "name"]
Simple, beautiful data without annotation or distraction.
1: By “path” here I’m talking about something like JSONPath — or XPath in the world of XML.↩︎
2: They’re aware of it.↩︎