TP

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 emailing me directly and get a 20% discount. See dates in October and November for trainings in London and New York.

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:

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

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

Published: Thursday, 16 August 2012, 12:21 AM
Author: Tomas Petricek
Typos: Send me a pull request!
Tags: functional, f#, research