Figure

A blog about Swift and iOS development.

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 Strings, 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 Strings and Ints 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 classes and structs:

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 inits… and some typealiases 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.↩︎


Hit me up on twitter (@jemmons) to continue the conversation.

Latest | Archives