Type Driven Development: implementing shouldIdle by Mark Seemann
Type Driven Development is an outside-in technique. Once you have the overall behaviour defined, you need to implement the details. Here's 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. With the overall behaviour in place, there's still work to be done.
From type inference of the higher-order functions' arguments, we know that we still need to implement functions with these signatures:
- ReadyData -> bool
- unit -> Timed<MessageHandler option>
- NoMessageData -> bool
- unit -> Timed<'a>
NoMessageData -> boolfunction. If you want to see how to implement the other three functions, you can watch my Type-Driven Development with F# Pluralsight course.
NoMessageData -> bool function is defined as the
shouldIdle argument to the transitionFromNoMessage higher-order function. The purpose of the shouldIdle function is to determine whether there's enough remaining time to idle.
Since we know the signature of the function, we can start by declaring it like this:
let shouldIdle (nm : NoMessageData) : bool =
Although it doesn't have to be called shouldIdle, in this case, I think the name is appropriate.
In order to determine if there's enough time left to idle, the function must know what time it is right now. Recall that, by design, PollingConsumer states are instantaneous, while transitions take time. The time a transition starts and stops is captured by a Timed<'a> value.
nm argument has the type
NoMessageData, which is an alias for
Timed<TimeSpan list>. The
Timed part contains information about when the transition into the No message state started and stopped. Since being in a state has no duration,
nm.Stopped represents the time when shouldIdle executes. That's part of the solution, so we can start typing:
let shouldIdle (nm : NoMessageData) : bool = nm.Stopped
This doesn't yet compile, because
nm.Stopped is a DateTimeOffset value, but the function is declared as returning bool.
If we imagine that we add the idle duration to the current time, it should gives us the time it'd be when idling is done. That time should be less than the time the Polling Consumer must exit:
let shouldIdle (nm : NoMessageData) : bool = nm.Stopped + idleDuration < stopBefore
This still doesn't compile because
stopBefore are undefined, but this is easy to fix: promote them to arguments:
let shouldIdle idleDuration stopBefore (nm : NoMessageData) : bool = nm.Stopped + idleDuration < stopBefore
If you paid attention to the previous article, you'll notice that this is exactly the same technique I used for the transitionFromNoMessage function. Apparently, we're not done with outside-in development yet.
Type inference #
The function now compiles, and has the type
TimeSpan -> DateTimeOffset -> NoMessageData -> bool.
Once again, I've used the trick of promoting an undefined value to a function argument, and let type inference take care of the rest. This also works here. Since
nm.Stopped is a DateTimeOffset value, and we're adding something to it with the
idleDuration has to be of a type that supports adding to DateTimeOffset. The only thing you can add to DateTimeOffset is a TimeSpan, so
idleDuration is inferred to be a TimeSpan value.
When you add a TimeSpan value to a DateTimeOffset value, you get another DateTimeOffset value back, so the type of the expression
nm.Stopped + idleDuration is DateTimeOffset. The entire return expression compares that DateTimeOffset value with the
< operator, which requires that both the left-hand and the right-hand expressions have the same type. Ergo must
stopBefore also be a DateTimeOffset value.
While we set out to implement a function with the type
NoMessageData -> bool, we eventually created a function with the type
TimeSpan -> DateTimeOffset -> NoMessageData -> bool, which isn't quite what we need.
Partial application #
The extra arguments can be removed again with partial function application. When the Polling Consumer application starts, it can easily calculate when it ought to stop:
let now' = DateTimeOffset.Now let stopBefore' = now' + TimeSpan.FromMinutes 1.
This assumes that the Polling Consumer should run for a maximum of 1 minute.
Likewise, we can create an idle duration value:
let idleDuration' = TimeSpan.FromSeconds 5.
Here, the value is hard-coded, but it could have gone in a configuration file instead, or be passed in as a command-line argument.
Given these values, we can now partially apply the function:
let shouldIdle' = shouldIdle idleDuration' stopBefore'
Since we're not supplying the third NoMessageData argument for the function, the return value of this partial application is a new function with the type
NoMessageData -> bool - exactly what we need.
In this article, you saw how to approach the implementation of one of the functions identified with the outside-in Type Driven Development technique. If you want to see the other three functions implemented, a much more detailed discussion of the technique, as well as the entire code base with commit messages, you can watch my Type-Driven Development with F# Pluralsight course.