Type Driven Development: composition by Mark Seemann
When you develop a system with an outside-in technique like Type Driven Development, you'll eventually have all the required building blocks. Now you need to compose them into an application. This post shows an example.
In my article about Type Driven Development, I demonstrated how to approach a problem in an iterative fashion, using the F# type system to do outside-in development, and in a follow-up article, I showed you how to implement one of the inferred methods. In this article, you'll see how to compose all the resulting building blocks into an application.
In the first article, you learned that apart from the functions defined in that article itself, you'd need four other functions:
- ReadyData -> bool
- unit -> Timed<MessageHandler option>
- NoMessageData -> bool
- unit -> Timed<'a>
As you can see in my Type-Driven Development with F# Pluralsight course, some of the other implementations turn out to have function arguments themselves. It's not quite enough with only those four functions. Still, the final number of implementation functions is only 9.
Here are all the building blocks (excluding the types and functions related to Timed<'a>):
- run : (PollingConsumer -> PollingConsumer) -> PollingConsumer -> PollingConsumer
- transition : (ReadyData -> bool) -> (unit -> Timed<MessageHandler option>) -> (NoMessageData -> bool) -> (unit -> Timed<'a>) -> PollingConsumer -> PollingConsumer
- shouldIdle : TimeSpan -> DateTimeOffset -> NoMessageData -> bool
- idle : TimeSpan -> unit -> Timed<unit>
- shouldPoll : (TimeSpan list -> TimeSpan) -> DateTimeOffset -> ReadyData -> bool
- poll : (unit -> 'a option) -> ('a -> 'b) -> (unit -> DateTimeOffset) -> unit -> Timed<MessageHandler option>
- calculateExpectedDuration : TimeSpan -> TimeSpan list -> TimeSpan
- simulatedPollForMessage : Random -> unit -> unit option
- simulatedHandle : Random -> unit -> unit
When the Polling Consumer starts, it can start by figuring out the current time, and calculate some derived values from that and some configuration values, as previously explained.
let now' = DateTimeOffset.Now let stopBefore' = now' + TimeSpan.FromMinutes 1. let estimatedDuration' = TimeSpan.FromSeconds 2. let idleDuration' = TimeSpan.FromSeconds 5.
estimatedDuration' value is a TimeSpan containing a (conservative) estimate of how long time it takes to handle a single message. It's only used if there are no already observed message handling durations, as the algorithm then has no statistics about the average execution time for each message. This value could come from a configuration system, or a command-line argument. In a recent system, I just arbitrarily set it to 2 seconds.
Given these initial values, we can compose all other required functions:
let shouldPoll' = shouldPoll (calculateExpectedDuration estimatedDuration') stopBefore' let r' = Random() let handle' = simulatedHandle r' let pollForMessage' = simulatedPollForMessage r' let poll' = poll pollForMessage' handle' Clocks.machineClock let shouldIdle' = shouldIdle idleDuration' stopBefore' let idle' = idle idleDuration' let transition' = transition shouldPoll' poll' shouldIdle' idle' let run' = run transition'
run' function has the type
PollingConsumer -> PollingConsumer. The input PollingConsumer value is the start state, and the function will return another PollingConsumer when it's done. Due to the way the
run function is implemented, this return value is always going to be StoppedState.
All that's left to do is to execute the
run' function with a ReadyState as the initial state:
let result' = run'(ReadyState( |> Timed.capture Clocks.machineClock))
A ReadyState value is required as input, and the ReadyState case constructor takes a ReadyData value as input. ReadyData is an alias for Timed<TimeSpan list>. An the beginning, the Polling Consumer hasn't observed any messages, so the TimeSpan list should be empty.
TimeSpan list must be converted to
Timed<TimeSpan list>, which can be done by piping it into
Timed.capture, using the machine clock.
When you execute the run' function, it may produce output like this:
Polling Sleeping Polling Sleeping Polling Handling Polling Sleeping Polling Sleeping Polling Sleeping Polling Handling Polling Sleeping Polling Sleeping Polling Sleeping Polling Sleeping Polling Handling Polling Handling Polling Sleeping Real: 00:00:59.392, CPU: 00:00:00.031, GC gen0: 0, gen1: 0, gen2: 0
This is because I set up some of the functions to print to the console so that we can see what's going on.
Notice that the state machine ran for 59 seconds and 392 milliseconds, exiting just before the minute was up.
Once you have all the appropriate building blocks, you can compose your desired system and run it. Notice how much this resembles the Composition Root pattern, only with functions instead of objects.
If you want to see more details about this example, or get access to the full source code, you can watch my Type-Driven Development with F# Pluralsight course. Please be aware that only certain subscription levels will give you source code access.