Adding Meaning to Swift Tuples with Enums
/One of the most interesting elements of Swift's announcement was the introduction of tuples and the ability to return more than one value from a function (obviously you can use them to pass more than one value too, but all of the below applies to that usage too).
var func myFunc()->(String,String){ return ("a","b") }
You can refer to the different elements of the tuple to access the first, third, etc element very easily too
//prints "a" println(myFunc().0)
For an ad-hoc function that is fine but when I design an API, even if I'm the only one that will ever use it, I'm always worried about how easily I will be able to work with it if I haven't touched it for a few months. Tuples do have something to add some meaning through naming the elements
var func myFunc()->(firstString:String,secondString:String){ return ("a","b") }
You can now refer to them as you would any other property
//prints "a" println(myFunc().0) //prints "b" println(myFunc().secondString)
At this point we have a very bespoke little behaviour free object (which is an interesting idea on its own). However, at this point you might ask yourself why this hadn't been defined as a struct or a class with meaningful initialisers that can ensure that values are always correctly configured. That's a fair question, and I think if you find yourself at this level then you probably need to at least consider those slightly heavier weight solutions.
That's exactly where we were with the tokeniser we have been developing. One of the key pieces of information that needed to be passed around was how states should change. The initial implementation documented in the blog post was purely stack based, and we wanted to extend it with a more state based solution. That meant that when processing characters in the input stream we wanted to pass a few different pieces of information back about how the state of the tokeniser should change.
- The state should not change
- The state should change to another state
- A valid token could be generated and the entire state machine should revert to the starting state
- An error has occurred and the current character cannot be processed in the current state, and a meaningful error token that may or may not be reported to the consumer of the token stream
A quick tuple would enable this (newState:State?, token:Token?). We can map this onto our different situations described above
- (newState:nil,token:nil)
- (newState:theNewState,token:nil)
- (newState:nil,token:theNewToken)
- (newState:nil,token:theErrorToken) (confirmed by result.token is Token.ErrorToken)
Nice and simple, however we have to remember all of these sentinel conditions. Even worse, it would be easy for me to write some code where for example newState has a state AND token. What does that mean? Perhaps I'm adding another condition, but how do I know where I have to deal with that condition (state transitions are thrown around all over the place)?
Swift enums come to the rescue here. Consider the TokenizationStateChange enum (this may be refined a little before we update the tokeniser publicly)
enum TokenizationStateChange{ //No state change requried case None //Leave this state case Exit //Leave this state, and there was an error case Error(errorToken:Token.ErrorToken) //Move to this new state case Transition(newState:TokenizationState) }
You can see the four cases here, but we have now given them real meaning. The first two cases look quite normal, but Error and Transition both carry additional information. Note we have got rid of optionals as well. I think that's important, the compiler can now make sure that we never return a transition to a nil state. Anyhow, how do we create a new enum of with this data, and how do we use it in, for example, a switch statement?
let mystateChange:TokenizationStateChange = TokenizationStateChange.Error( errorToken:Token.ErrorToken("Error message") )
It's very much like create a new instance of a class, the tuple feels a bit like a constructor and we just supply the required object to it with the correct label. The compiler will warn us if we get it wrong (it should be noted that I didn't need to declare the type, but I want the compiler to do the hard work for me rather than me doing the checking).
The real power comes when you want to work with these enums as values of variables or constants. Firstly, if you aren't interested in the attached tuple, but just the overall state, there is no difference to working with a normal enum.
switch consumptionResult!{ case .Error,.Exit: println("Something is done or broken") case .Transition: println("Just move on to a new state!") case .None: println("As you were") }
So we can ignore the information about the state if we don't care. If we add a new case to the enum, we'll get an error message telling us we aren't dealing with all possible cases AND I can easily see which code is responding to which states, and what the states mean. Of course we can take it further. Here's how our default tokeniser deals with a state change
switch stateChange!{ case .Error(let errorToken): processToken(errorToken) error = true case .Exit: currentState = self return consume(character, controller:controller) case .Transition(let newState): currentState = newState return consume(character, controller: controller) case .None: storedCharacters += "\(character)" return TokenizationStateChange.None }
You can see that we declare that a constant containing the first value of the tuple should be set up for the .Transition and .Exit states. This then enables you to work with those values, using them like any normal constant. We now have a well formed, defined, compiler checkable light-weight object to return that has both meaning and just enough information for each state.
There are a couple of other restrictions that you should be aware of
- You cannot (in a switch statement) have multiple cases that deal with both tupled cases and non tupled cases (e.g. Error and Exit) if you want to use the value. That is you can't have
case .Exit,.Error(let errorToken):
What should the value of errorToken be if it's an .Exit case? Exactly. - You cannot fall through from a non-tupled case to a tuple'd case. Really it results in a sub-set of the above, but it's worth noting.
We've found this a really powerful pattern, giving tuples some real meaning that both the compiler can check, and we as humans can understand.
We'd love to hear your thoughts.