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
You've seen the development of scoreWhenDeuce, scoreWhenAdvantage, and scoreWhenForty in previous articles, but you haven't seen scoreWhenGame or scoreWhenPoints. The development of these remaining functions follow similar principles, and use similar techniques. If you're interested in the details, you can always peruse the source code repository.

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 PointsDataForty of FortyDataDeuceAdvantage of PlayerGame 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.

In the next article, you'll see how to define the initial state, and in the article beyond that, you'll see how to move through a sequence of transitions.

If you're interested in learning more about designing with types, you can watch my Type-Driven Development with F# Pluralsight course.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Tuesday, 16 February 2016 14:23:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Tuesday, 16 February 2016 14:23:00 UTC