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

F# trainings offer Learn more about the ideas discussed in this article in my F# and functional programming training. As a special offer for the readers of this blog, register by 20% discount. See London and New York. Learn more about the ideas discussed in this article in my F# and functional programming training. As a special offer for the readers of this blog, register by emailing me directly and get a. See dates in October and November for trainings inand

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 - an article that inspired this one. It describes the use of types from a more advanced perspective, mainly for understanding behaviour of programs.

namespace System

type MeasureAttribute =

class

inherit Attribute

new : unit -> MeasureAttribute

end



Full name: Microsoft.FSharp.Core.MeasureAttribute



type: MeasureAttribute

implements: Runtime.InteropServices._Attribute

inherits: Attribute



[<Measure>]

type km



Full name: Blog.km

[<Measure>]

type litre



Full name: Blog.litre

[<Measure>]

type GBP



Full name: Blog.GBP

type PricePerUnit = float<GBP/litre>



Full name: Blog.PricePerUnit



type: PricePerUnit

implements: IComparable

implements: IConvertible

implements: IFormattable

implements: IComparable<PricePerUnit>

implements: IEquatable<PricePerUnit>

inherits: ValueType



Multiple items

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



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



union case Fuel.Biodiesel: Fuel

union case Fuel.Diesel: Fuel

union case Fuel.LPG: Fuel

union case Fuel.Gasoline: Fuel

Multiple items

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



Refueling

type Journey =

{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



Journey.Start: DateTime

type DateTime =

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



Journey.Driver: string

Multiple items

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.Distance: float<km>

Multiple items

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



type 'T list = List<'T>



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



type RefuelingKind =

| 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



union case RefuelingKind.FillTank: PricePerUnit -> RefuelingKind

union case RefuelingKind.FillCanister: PricePerUnit -> RefuelingKind

union case RefuelingKind.UseCanister: RefuelingKind

type Refueling =

{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.Counter: float<km>

Multiple items

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



Refueling.Amount: float<litre>

Refueling.Kind: RefuelingKind

type PricePerUnit = float<GBP/litre>



Full name: Blog.Snippet2.PricePerUnit



type: PricePerUnit

implements: IComparable

implements: IConvertible

implements: IFormattable

implements: IComparable<PricePerUnit>

implements: IEquatable<PricePerUnit>

inherits: ValueType



type Fuel =

| 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



type JourneyLog = obj



Full name: Blog.JourneyLog

type TimeSpan =

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

