Why type-first development matters
Using functional programming language changes the way you write code in a number of ways. Many of the changes are at a small-scale. For example, you learn how to express computations in a shorter, more declarative way using higher-order functions. However, there are also many changes at a large-scale. The most notable one is that, when designing a program, you start thinking about the (data) types that represent the data your code works with.
In this article, I describe this approach. Since the acronym TDD is already taken, I call the approach Type-First Development (TFD), which is probably a better name anyway. The development is not driven by types. It starts with types, but the rest of the implementation can still use test-driven development for the implementation.
This article demonstrates the approach using a case study from a real life: My example is a project that I started working on with a friend who needed a system to log journeys with a company car (for expense reports). Using the type-first approach made it easier to understand and discuss the problem.
In many ways, TFD is a very simple approach, so this article just gives a name to a practice that is quite common among functional and F# programmers (and we have been teaching it at our F# trainings for the last year).
Preamble
Ideas for this article occurred to me when reading an interesting post on type driven design. The article discusses quite different aspects of types, but it starts with an inspiring quote:
When writing code in a statically typed language sometimes types are considered as orthogonal to the logic of the code. We write them to appease the compiler, or get performance or IntelliSense & navigation, but all of these has no relation to the code itself.
This is wrong.
How do I use types when writing code in F#? First of all, I use them as a useful notation for quickly and easily writing down ideas about program design and as a specification of a program. And I believe this is the case for many other F# programmers. In fact, this use of types is not limited to statically-typed languages. I think it applies similarly to other functional languages, although Scheme or LISP programmers would probably talk about data structures instead of types.
Case study: Journey log
Let's start the article with an example. As I said earlier, this is based on a real discussion that I had with a friend, so I will try to replay the discussion. I was writing the F# code as we discussed, but I did not know much about the domain. A friend understands the domain (and knows C# and some Haskell, but not F#, though I believe the F# code should be understandable to less technical people too).
Designing Journey log types
The purpose of the application is to calculate how expensive were various car journeys, how much money was spent for different cars etc. However, the first question is, what data the application processes? It needs to record date when a journey started, driver's name, total distance and, most importantly, a list of refuelings. In F#, this can be written as the following (record) type:
type Refueling = (...) type Journey = { Start : DateTime Driver : string Distance : float<km> Refueling : list<Refueling> }
At this point, we do not know what Refueling
represents, but we could already
define a type representing Journey
. Note that I use a type float<km>
for distance -
this is using F# units of measure, which make it possible to attach unit information
to types (and check them at compile time), but it also serves as a useful documentation.
The next question is, what information does the application have about refuelings?
In other words, what is the declaration of the Refueling
type. Here, the story is
more interesting:
- We can fill the tank (in which case we want to log the amount and the price)
- We can fill a canister (and we need to log the same information as in the previous point)
- We can use a canister (in which case we log everything except for a price)
For all refuelings, we need to log the state of the total kilometer counter (showing the total number of kilometers traveled so far in the car) and fuel (in some cars you can combine biodiesel and diesel, etc.) This leads us to the following F# type declarations:
type RefuelingKind = | FillTank of PricePerUnit | FillCanister of PricePerUnit | UseCanister type Refueling = { Counter : float<km> Fuel : Fuel Amount : float<litre> Kind : RefuelingKind }
For the Refueling
type is again written as a record that combines all information we want
to keep for every refueling. The specific details are captured by RefuelingKind
. This is
an F# discriminated union - a type that can have either FillTank
value, FillCanister
value
or UseCanister
value. For the first two cases, we need to keep the price per litre.
The two remaining types, Fuel
and PricePerUnit
can be defined as follows:
type PricePerUnit = float<GBP / litre> type Fuel = | Biodiesel | Diesel | LPG | Gasoline
The PricePerUnit
type is a type alias specifying that price is measured in GBP per litre.
Again, we can use F# units of measure to capture this additional information (later in the
implementation, the compiler will check that we use the number correctly and, i.e. always
multiply it by amount when calculating total price). The Fuel
type is a simple
discriminated union that simply lists the different fuels (although we might later want
to change it and make the system more extensible).
Reading data types
What have we achieved so far? We described the domain model for the Journey log application with less than 20 lines of code. When writing the code for the first time, I typed a few type declarations, explained them to my friend and he gave me feedback saying what was wrong with my first design. This means that the process was quite collaborative and iterative.
If we were doing that on a whiteboard, we would probably draw diagram like the one on the right. You can see that the F# types quite closely correspond to what you would draw as a UML diagram (although the detailed meaning is a bit different).
However, there is one major benefit in writing the ideas down as code. The code is executable. As you develop the application, you will add documentation, evolve the code in many ways, but the core domain can still stay as a reasonably short single file in your project and it will always be in sync. New developers joining the project (or a person coming back to it after some time) can just read that single file and get a very good idea about the project.
Adding Journey log functionality
The discussion so far was focused on types that are used to represent the data that the application works with. However, type-first development is equally useful if we want to focus on the behaviour. What kind of operations we expect from the Journey log application?
One task that we need for producing expenses is to calculate how much was spent on individual fuels in a given month (or, perhaps an arbitrary date range). Another important task is to calculate what the average prices per kilometer for LPG, Gasoline and other fuels are. In a type-first development, we start by writing the types of the functions:
type JourneyLog = list<Journey> /// Calculate amount spent on fuel in a given period val reportExpenses : JourneyLog -> DateTime * TimeSpan -> Fuel -> float<GBP> /// Calculate average price of fuel per kilometer traveled val averagePrice : JourneyLog -> Fuel -> float<GBP / kilometer>
The code first defines JourneyLog
type, which represents a list of journeys. The operations
that work with journey log always take JourneyLog
as their first argument.
The function reportExpenses
takes DateTime * TimeSpan
, which represents a date range and
the kind of Fuel
we are interested in. The result is a price in pounds, represented
as float<GBP>
. Units of measure are again very useful - they clearly inform us that the
result is price (as opposed to, i.e. fuel consumption, which would be float<litre/kilometer>
).
The function averagePrice
takes journey log, fuel and calculates a price per kilometer
(as specified by the type float<GBP / kilometer>
).
Reading function types
By looking at the type signature, you can get a reasonably good idea about what the operation might do (this is even more the case for higher-order functions as discussed in the type driven design article).
One important aspect of function types is that they tell you what results you can
get from what inputs (and how). The diagram on the right demonstrates this in a
bit more complicated scenario. Aside from JourneyLog
, we also have a type ExpenseList
that represents a list of expense reports.
If you have, somewhere in the program, a value of type JourneyLog
and want to print
(obtain) the Invoice
, the types tell you what you can do. First, you need to turn
JourneyLog
into ExpenseList
(using the function calculateExpenses
) and then you
can obtain Invoice
using printInvoice
.
This is a fairly simple example, but it demonstrates an important fact. If you use
types to represent different data structures that your code uses, the types of
functions tell you how you can process such data structures. When reading code, you
can often use function types to understand what is happening. For example, in the
diagram, we can only go from JourneyLog
to ExpenseList
, so the function probably
only uses some of the information from journey log. However, it is worth storing the
result in a separate data type (ExpenseList
), so the expense list is probably used
in a number of ways in the application.
Type-First Development
The example above demonstrated what I call type-first development (TFD). As discussed earlier, I believe this is a method that many users of F# and similar languages (perhaps more Haskell than Scala) use in practice. Let me now describe a few key points about this approach.
1. TFD emphasizes and simplifies communication
When you use TFD, you get a very simple specification or documentation for your software system very early during the development process (without even starting Microsoft Word). You get a short piece of code that you can discuss with your colleagues, send around by email (which is a lot more difficult with diagrams).
I'm not saying that you'll be able to show type declarations to your senior management or to the end users of your system, but you usually can show them to any technical person. They might not be able to read the code themselves, but you can walk through the code and explain it.
In summary, this means that TFD supports communication and collaboration (perhaps in a developer-friendly way). The importance of communication is clear and is emphasized by other techniques like BDD.
2. TFD enables quick prototyping
The code that you need to write to get a complete list of definitions that model your problem domain is very short (at least, in languages like F#). This means that you do not have to worry about doing it wrong. You can start by writing down some types and if you see they do not correspond to your domain, simply throw the first version away.
When you have data types describing your domain and function types describing the behaviour, you can start implementing some of the functions, but it is similarly easy to write an incomplete, mock implementation just to have something you can test.
3. TFD works well with testing
This leads us to the next point. Type-first development (TFD) works very well with
test-driven development (TDD). In some way, when writing types, we are writing only
code that is necessary to write our first tests. Once we write the type of a function
such as averagePrice
, you can start writing tests for the function and based on these
tests, you can then fill-in the implementation of the function.
In fact, you can read types as very basic tests. The type of the averagePrice
function is a test checking that when you call the function with correct JourneyLog
and
Fuel
, it gives you some number representing price in GBP per kilometers. This is
very general test, but it gives us useful guarantees and we can continue by adding
other tests, verifying the behaviour for concrete journey logs.
4. TFD supports extensibility
Focusing on the types used to represent data makes it easier to add new functionality
later on during the development. Once we have the JourneyLog
type, we can add new functions
that process it and calculate new statistics or reports from the data.
Writing such new processing functionality is quite easy, because new code is going to be very localized. We do not need to modify any existing objects. We just write a new function to process the data (possibly using some other functions that we already have).
Summary
In this article, I described a development style that often comes with functional programming languages such as F#. It was partly inspired by a related article on type driven design, but I discussed the topic from a different perspective - instead of looking at types technically, I tried to highlight what they mean in practice, for the development style.
The key idea of the type-first development (TFD) is that we start designing and prototyping applications by writing the types they work with. In F#, this is done by writing type declarations (using records and discriminated unions), but the same methodology can work in other languages - even in dynamically typed ones.
As demonstrated by a simple case study, focusing on the types first gives you a very powerful way to communicate and prototype ideas about design. Using types is very developer-friendly approach, but I believe that it is accessible to any somewhat technical person. Moreover, the methodology works well with test-driven development style and it helps writing extensible code.
Of course, no one size fits all. There are clearly scenarios where TFD is not the right way. As mentioned, it is very developer centric and so if you need to work with non-technical analysts or customers on a complex projects, approaches like behaviour-driven development (BDD) may be more relevant.
References
- Luca Cardelli: Typeful Programming (PDF) - describes the idea of typeful programming, which is similar to what is discussed in this article. It emphasizes types and describes various sophisticated uses.
- Ittay Dror: Type Driven Design (archived from) - an article that inspired this one. It describes the use of types from a more advanced perspective, mainly for understanding behaviour of programs.
class
inherit Attribute
new : unit -> MeasureAttribute
end
Full name: Microsoft.FSharp.Core.MeasureAttribute
type: MeasureAttribute
implements: Runtime.InteropServices._Attribute
inherits: Attribute
type km
Full name: Blog.km
type litre
Full name: Blog.litre
type GBP
Full name: Blog.GBP
Full name: Blog.PricePerUnit
type: PricePerUnit
implements: IComparable
implements: IConvertible
implements: IFormattable
implements: IComparable<PricePerUnit>
implements: IEquatable<PricePerUnit>
inherits: ValueType
val float : 'T -> float (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.float
--------------------
type float<'Measure> = float
Full name: Microsoft.FSharp.Core.float<_>
type: float<'Measure>
implements: IComparable
implements: IConvertible
implements: IFormattable
implements: IComparable<float<'Measure>>
implements: IEquatable<float<'Measure>>
inherits: ValueType
--------------------
type float = Double
Full name: Microsoft.FSharp.Core.float
type: float
implements: IComparable
implements: IFormattable
implements: IConvertible
implements: IComparable<float>
implements: IEquatable<float>
inherits: ValueType
| Biodiesel
| Diesel
| LPG
| Gasoline
Full name: Blog.Fuel
type: Fuel
implements: IEquatable<Fuel>
implements: Collections.IStructuralEquatable
implements: IComparable<Fuel>
implements: IComparable
implements: Collections.IStructuralComparable
union case Refueling.Refueling: Refueling
--------------------
type Refueling = | Refueling
Full name: Blog.Snippet1.Refueling
type: Refueling
implements: IEquatable<Refueling>
implements: Collections.IStructuralEquatable
implements: IComparable<Refueling>
implements: IComparable
implements: Collections.IStructuralComparable
{Start: DateTime;
Driver: string;
Distance: float<km>;
Refueling: Refueling list;}
Full name: Blog.Snippet1.Journey
type: Journey
implements: IEquatable<Journey>
implements: Collections.IStructuralEquatable
implements: IComparable<Journey>
implements: IComparable
implements: Collections.IStructuralComparable
struct
new : int64 -> System.DateTime
new : int64 * System.DateTimeKind -> System.DateTime
new : int * int * int -> System.DateTime
new : int * int * int * System.Globalization.Calendar -> System.DateTime
new : int * int * int * int * int * int -> System.DateTime
new : int * int * int * int * int * int * System.DateTimeKind -> System.DateTime
new : int * int * int * int * int * int * System.Globalization.Calendar -> System.DateTime
new : int * int * int * int * int * int * int -> System.DateTime
new : int * int * int * int * int * int * int * System.DateTimeKind -> System.DateTime
new : int * int * int * int * int * int * int * System.Globalization.Calendar -> System.DateTime
new : int * int * int * int * int * int * int * System.Globalization.Calendar * System.DateTimeKind -> System.DateTime
member Add : System.TimeSpan -> System.DateTime
member AddDays : float -> System.DateTime
member AddHours : float -> System.DateTime
member AddMilliseconds : float -> System.DateTime
member AddMinutes : float -> System.DateTime
member AddMonths : int -> System.DateTime
member AddSeconds : float -> System.DateTime
member AddTicks : int64 -> System.DateTime
member AddYears : int -> System.DateTime
member CompareTo : obj -> int
member CompareTo : System.DateTime -> int
member Date : System.DateTime
member Day : int
member DayOfWeek : System.DayOfWeek
member DayOfYear : int
member Equals : obj -> bool
member Equals : System.DateTime -> bool
member GetDateTimeFormats : unit -> string []
member GetDateTimeFormats : System.IFormatProvider -> string []
member GetDateTimeFormats : char -> string []
member GetDateTimeFormats : char * System.IFormatProvider -> string []
member GetHashCode : unit -> int
member GetTypeCode : unit -> System.TypeCode
member Hour : int
member IsDaylightSavingTime : unit -> bool
member Kind : System.DateTimeKind
member Millisecond : int
member Minute : int
member Month : int
member Second : int
member Subtract : System.DateTime -> System.TimeSpan
member Subtract : System.TimeSpan -> System.DateTime
member Ticks : int64
member TimeOfDay : System.TimeSpan
member ToBinary : unit -> int64
member ToFileTime : unit -> int64
member ToFileTimeUtc : unit -> int64
member ToLocalTime : unit -> System.DateTime
member ToLongDateString : unit -> string
member ToLongTimeString : unit -> string
member ToOADate : unit -> float
member ToShortDateString : unit -> string
member ToShortTimeString : unit -> string
member ToString : unit -> string
member ToString : string -> string
member ToString : System.IFormatProvider -> string
member ToString : string * System.IFormatProvider -> string
member ToUniversalTime : unit -> System.DateTime
member Year : int
static val MinValue : System.DateTime
static val MaxValue : System.DateTime
static member Compare : System.DateTime * System.DateTime -> int
static member DaysInMonth : int * int -> int
static member Equals : System.DateTime * System.DateTime -> bool
static member FromBinary : int64 -> System.DateTime
static member FromFileTime : int64 -> System.DateTime
static member FromFileTimeUtc : int64 -> System.DateTime
static member FromOADate : float -> System.DateTime
static member IsLeapYear : int -> bool
static member Now : System.DateTime
static member Parse : string -> System.DateTime
static member Parse : string * System.IFormatProvider -> System.DateTime
static member Parse : string * System.IFormatProvider * System.Globalization.DateTimeStyles -> System.DateTime
static member ParseExact : string * string * System.IFormatProvider -> System.DateTime
static member ParseExact : string * string * System.IFormatProvider * System.Globalization.DateTimeStyles -> System.DateTime
static member ParseExact : string * string [] * System.IFormatProvider * System.Globalization.DateTimeStyles -> System.DateTime
static member SpecifyKind : System.DateTime * System.DateTimeKind -> System.DateTime
static member Today : System.DateTime
static member TryParse : string * System.DateTime -> bool
static member TryParse : string * System.IFormatProvider * System.Globalization.DateTimeStyles * System.DateTime -> bool
static member TryParseExact : string * string * System.IFormatProvider * System.Globalization.DateTimeStyles * System.DateTime -> bool
static member TryParseExact : string * string [] * System.IFormatProvider * System.Globalization.DateTimeStyles * System.DateTime -> bool
static member UtcNow : System.DateTime
end
Full name: System.DateTime
type: DateTime
implements: IComparable
implements: IFormattable
implements: IConvertible
implements: Runtime.Serialization.ISerializable
implements: IComparable<DateTime>
implements: IEquatable<DateTime>
inherits: ValueType
val string : 'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = String
Full name: Microsoft.FSharp.Core.string
type: string
implements: IComparable
implements: ICloneable
implements: IConvertible
implements: IComparable<string>
implements: seq<char>
implements: Collections.IEnumerable
implements: IEquatable<string>
Journey.Refueling: Refueling list
--------------------
type Refueling = | Refueling
Full name: Blog.Snippet1.Refueling
type: Refueling
implements: IEquatable<Refueling>
implements: Collections.IStructuralEquatable
implements: IComparable<Refueling>
implements: IComparable
implements: Collections.IStructuralComparable
Full name: Microsoft.FSharp.Collections.list<_>
type: 'T list
implements: Collections.IStructuralEquatable
implements: IComparable<List<'T>>
implements: IComparable
implements: Collections.IStructuralComparable
implements: Collections.Generic.IEnumerable<'T>
implements: Collections.IEnumerable
| FillTank of PricePerUnit
| FillCanister of PricePerUnit
| UseCanister
Full name: Blog.RefuelingKind
type: RefuelingKind
implements: IEquatable<RefuelingKind>
implements: Collections.IStructuralEquatable
implements: IComparable<RefuelingKind>
implements: IComparable
implements: Collections.IStructuralComparable
{Counter: float<km>;
Fuel: Fuel;
Amount: float<litre>;
Kind: RefuelingKind;}
Full name: Blog.Refueling
type: Refueling
implements: IEquatable<Refueling>
implements: Collections.IStructuralEquatable
implements: IComparable<Refueling>
implements: IComparable
implements: Collections.IStructuralComparable
Refueling.Fuel: Fuel
--------------------
type Fuel =
| Biodiesel
| Diesel
| LPG
| Gasoline
Full name: Blog.Fuel
type: Fuel
implements: IEquatable<Fuel>
implements: Collections.IStructuralEquatable
implements: IComparable<Fuel>
implements: IComparable
implements: Collections.IStructuralComparable
Full name: Blog.Snippet2.PricePerUnit
type: PricePerUnit
implements: IComparable
implements: IConvertible
implements: IFormattable
implements: IComparable<PricePerUnit>
implements: IEquatable<PricePerUnit>
inherits: ValueType
| Biodiesel
| Diesel
| LPG
| Gasoline
Full name: Blog.Snippet2.Fuel
type: Fuel
implements: IEquatable<Fuel>
implements: Collections.IStructuralEquatable
implements: IComparable<Fuel>
implements: IComparable
implements: Collections.IStructuralComparable
Full name: Blog.JourneyLog
struct
new : int64 -> System.TimeSpan
new : int * int * int -> System.TimeSpan
new : int * int * int * int -> System.TimeSpan
new : int * int * int * int * int -> System.TimeSpan
member Add : System.TimeSpan -> System.TimeSpan
member CompareTo : obj -> int
member CompareTo : System.TimeSpan -> int
member Days : int
member Duration : unit -> System.TimeSpan
member Equals : obj -> bool
member Equals : System.TimeSpan -> bool
member GetHashCode : unit -> int
member Hours : int
member Milliseconds : int
member Minutes : int
member Negate : unit -> System.TimeSpan
member Seconds : int
member Subtract : System.TimeSpan -> System.TimeSpan
member Ticks : int64
member ToString : unit -> string
member ToString : string -> string
member ToString : string * System.IFormatProvider -> string
member TotalDays : float
member TotalHours : float
member TotalMilliseconds : float
member TotalMinutes : float
member TotalSeconds : float
static val TicksPerMillisecond : int64
static val TicksPerSecond : int64
static val TicksPerMinute : int64
static val TicksPerHour : int64
static val TicksPerDay : int64
static val Zero : System.TimeSpan
static val MaxValue : System.TimeSpan
static val MinValue : System.TimeSpan
static member Compare : System.TimeSpan * System.TimeSpan -> int
static member Equals : System.TimeSpan * System.TimeSpan -> bool
static member FromDays : float -> System.TimeSpan
static member FromHours : float -> System.TimeSpan
static member FromMilliseconds : float -> System.TimeSpan
static member FromMinutes : float -> System.TimeSpan
static member FromSeconds : float -> System.TimeSpan
static member FromTicks : int64 -> System.TimeSpan
static member Parse : string -> System.TimeSpan
static member Parse : string * System.IFormatProvider -> System.TimeSpan
static member ParseExact : string * string * System.IFormatProvider -> System.TimeSpan
static member ParseExact : string * string [] * System.IFormatProvider -> System.TimeSpan
static member ParseExact : string * string * System.IFormatProvider * System.Globalization.TimeSpanStyles -> System.TimeSpan
static member ParseExact : string * string [] * System.IFormatProvider * System.Globalization.TimeSpanStyles -> System.TimeSpan
static member TryParse : string * System.TimeSpan -> bool
static member TryParse : string * System.IFormatProvider * System.TimeSpan -> bool
static member TryParseExact : string * string * System.IFormatProvider * System.TimeSpan -> bool
static member TryParseExact : string * string [] * System.IFormatProvider * System.TimeSpan -> bool
static member TryParseExact : string * string * System.IFormatProvider * System.Globalization.TimeSpanStyles * System.TimeSpan -> bool
static member TryParseExact : string * string [] * System.IFormatProvider * System.Globalization.TimeSpanStyles * System.TimeSpan -> bool
end
Full name: System.TimeSpan
type: TimeSpan
implements: IComparable
implements: IComparable<TimeSpan>
implements: IEquatable<TimeSpan>
implements: IFormattable
inherits: ValueType
Published: Thursday, 16 August 2012, 12:21 AM
Author: Tomas Petricek
Typos: Send me a pull request!
Tags: functional, f#, research