Types + Properties = Software: composition by Mark Seemann
In which a general transition function is composed from specialised transition functions.
This article is the fifth in a series of articles that demonstrate how to develop software using types and properties. In the previous article, you witnessed the continued walk-through of the Tennis Kata done with Property-Based Test-Driven Development. In these articles, you saw how to define small, specific functions that model the transition out of particular states. In this article, you'll see how to compose these functions to a more general function.
The source code for this article series is available on GitHub.
Composing the general function
If you recall the second article in this series, what you need to implement is a state transition of the type
Score -> Player -> Score. What you have so far are the following functions:
scoreWhenPoints : PointsData -> Player -> Score
scoreWhenForty : FortyData -> Player -> Score
scoreWhenDeuce : Player -> Score
scoreWhenAdvantage : Player -> Player -> Score
scoreWhenGame : Player -> Score
These five functions are all the building blocks you need to implement the desired function of the type
Score -> Player -> Score. You may recall that Score is a discriminated union defined as:
type Score = | Points of PointsData | Forty of FortyData | Deuce | Advantage of Player | Game of Player
Notice how these cases align with the five functions above. That's not a coincidence. The driving factor behind the design of these five function was to match them with the five cases of the Score type. In another article series, I've previously shown this technique, applied to a different problem.
You can implement the desired function by clicking the pieces together:
let score current winner = match current with | Points p -> scoreWhenPoints p winner | Forty f -> scoreWhenForty f winner | Deuce -> scoreWhenDeuce winner | Advantage a -> scoreWhenAdvantage a winner | Game g -> scoreWhenGame g
There's not a lot to it, apart from matching on
current. If, for example,
current is a Forty value, the match is the Forty case, and
f represents the FortyData value in that case. The destructured
f can be passed as an argument to scoreWhenForty, together with
winner. The scoreWhenForty function returns a Score value, so the
score function has the type
Score -> Player -> Score - exactly what you want!
Here's an example of using the function:
> score Deuce PlayerOne;; val it : Score = Advantage PlayerOne
When the score is deuce and player one wins the ball, the resulting score is advantage to player one.
Properties for the score function
Can you express some properties for the score function? Yes and no. You can't state particularly interesting properties about the function in isolation, but you can express meaningful properties about sequences of scores. We'll return to that in a later article. For now, let's focus on the function in itself.
You can, for example, state that the function can handle all input:
[<Property>] let ``score returns a value`` (current : Score) (winner : Player) = let actual : Score = score current winner true // Didn't crash - this is mostly a boundary condition test
This property isn't particularly interesting. It's mostly a smoke test that I added because I thought that it might flush out boundary issues, if any exist. That doesn't seem to be the case.
You can also add properties that examine each case of input:
[<Property>] let ``score Points returns correct result`` points winner = let actual = score (Points points) winner let expected = scoreWhenPoints points winner expected =! actual
Such a property is unlikely to be of much use, because it mostly reproduces the implementation details of the score function. Unless you're writing high-stakes software (e.g. for medical purposes), such properties are likely to have little or negative value. After all, tests are also code; do you trust the test code more than the production code? Sometimes, you may, but if you look at the source code for the score function, it's easy to review.
You can write four other properties, similar to the one above, but I'm going to skip them here. They are in the source code repository, though, so you're welcome to look there if you want to see them.
To be continued...
In this article, you saw how to compose the five specific state transition functions into an overall state transition function. This is only a single function that calculates a score based on another score. In order to turn this function into a finite state machine, you must define an initial state and a way to transition based on a sequence of events.
If you're interested in learning more about designing with types, you can watch my Type-Driven Development with F# Pluralsight course.