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.

Building blocks #

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>
In the article about implementation, you saw how to implement one of these functions: the shouldIdle function.

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
You may have noticed that some of these function names are prefixed with simulated. This is because I wrote some functions that only simulate that messages arrive and are handled. That's also the reason for the Random value here and there.

Composition #

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.

The 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'

The composed 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.

Execution #

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.

The 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.

Summary #

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.



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

Wednesday, 12 August 2015 07:24:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Wednesday, 12 August 2015 07:24:00 UTC