Dependent Types To Eliminate Runtime Checks

Moving runtime checks to the compile-time using Dependent Types

One of the main advantages of static type checking is to catch errors before we deploy code to production. Dependent Types allow us to eliminate some checks that are usually done at run time. Through a simple example, this article demonstrates how dependent types can be applied in everyday programming.

A person checking items off a checklist, symbolizing compile-time checks replacing runtime checks.
Photo by Glenn Carstens-Peters on Unsplash

Give Me The Money

Let us start with an example. We will define a simple type for representing money and a function to add money.

1data Money = Money Rational

Now we can add them together as

1add :: Money -> Money -> Money
2add (Money m1) (Money m2)  = Money (m1 + m2)
3
4money1 = Money (50 % 100)
5money2 = Money (25 % 100)
6
7total = add money1 money2

Everything OK There?

We’ve overlooked a crucial detail: currency. Money is always denominated in a specific currency (e.g., US Dollar), and it’s invalid to add amounts from different currencies.

Let’s include currency information in our Money type. A simple approach is to keep a string representing the currency (“GBP”, “USD”, etc.) along with the amount.

1data Money = Money String Rational
2
3twoDollars :: Money
4twoDollars = Money "USD" 2

Houston, We’ve Got a Problem

The constraint that only Money values with same currency can be added makes our add function partial.

1add:: Money -> Money -> Money
2add   (Money c1 m1) (Money c2 m2) =
3      case c1 == c2 of
4          True  ->  Money c1 (m2 + m2)
5      --  False ->  ??  -- the function is not defined for this value 

Partial functions are bad. They pass type check and fail at runtime. The application crashes when inputs for which the function is not defined are passed. Our add function above fails if it is ever called with two Money values with different currency.

1λ> :t add fiftyPence twoDollars
2add fiftyPence twoDollars :: Money
3
4λ> add fiftyPence twoDollars
5*** Exception: /Users/vj/workspace/Haskell/Money.hs:(23,7)-(24,37): Non-exhaustive patterns in case

Improvise, Adapt, Overcome

A common approach to resolve this is to extend the function, making it return a specific value (an error value) for inputs where it’s not well-defined.

Wrapping the return type with Maybe or Either e are common approaches to extending the function. A special value of Nothing::Maybe Money (or Left "Incompatible Currency"::Either String Money) can be returned for all the inputs where add is originally not defined.

If we use Maybe Money or Either Error Money as the return type, the calling function must handle these potential error values (perhaps by propagating the error to its own caller).

1add :: Money -> Money -> Maybe Money
2add   (Money c1 m1) (Money c2 m2) = 
3      case c1 == c2 of
4          True  -> Just (Money c1 (m1 + m2))
5          False -> Nothing

Independence? Freedom? What?

Using dependent types we can avoid the runtime check by making it possible for the compiler to do that check for us. Since the compiler prohibits calling our add function with incompatible money values, we are not only freed from runtime check, we also don’t have to worry about error handling.

This is in contrast to extending the function. Here we restrict the domain of the function to contain only those values for which the function is defined.

Tip
Dependent Types are advanced concepts. But we don’t have to know all the theory and concepts behind it as many good people have made it so simple for us to use it. But I recommend you all to read and know more to use them in more interesting ways.
 1{-# LANGUAGE KindSignatures, DataKinds #-}
 2
 3import GHC.TypeLits (Symbol)
 4import Data.Ratio ((%))
 5
 6data Money (currency :: Symbol) = Money Rational
 7
 8fiftyPence :: Money "GBP"
 9fiftyPence = Money (50 % 100)
10
11twoDollars :: Money "USD"
12twoDollars = Money 2
13
14add :: Money c -> Money c -> Money c
15add (Money m1) (Money m2) = Money (m1 + m2)

Notice how the implementation of add function is same as the one before introducing currency parameter. It has no if/case expressions and no Maybe return type. It is a compilation error to add Money values with different currency.

 1λ> :t add fiftyPence fiftyPence
 2add fiftyPence fiftyPence :: Money "GBP"
 3
 4λ> :t add fiftyPence twoDollars
 5
 6<interactive>:1:16: error:
 7Couldn't match type"USD"’ with ‘"GBP" 8      Expected type: Money "GBP"
 9        Actual type: Money "USD"
10In the second argument of ‘add’, namely ‘twoDollars’
11      In the expression: add fiftyPence twoDollars

We use language extensions in GHC for using dependent types. I will try to explain their usage in simple terms.

Be Kind To Others

Notice the type of fiftyPence -

1fiftyPence :: Money "GBP"

Money is a type constructor, as we know, type constructors only accepts other types as parameters (Maybe Int, [Int] etc.). But, "GBP" looks like a String value! How is this possible?

Firstly, "GBP" in fiftyPence :: Money "GBP" is not a value but a type with kind Symbol. You can see that in the data definition of Money. currency type parameter is of kind Symbol.

1data Money (currency :: Symbol) = Money Rational

Symbol is a convenient Kind provided by GHC in the GHC.TypeLits module which is the kind for type-level strings. It lets us use string literals like “GBP” as a type.

Info
You know that we can manually specify a variable’s type. Similarly, we can also manually specify a type variable’s kind using the KindSignatures extension.

Congratulations! You have Been Promoted

OK, So, KindSignatures extension lets me specify that currency has kind Symbol, but how does "GBP" and "USD" become types of of kind Symbol?

1fiftyPence :: Money "GBP"
2fiftyPence = Money (50 % 100)
3
4twoDollars :: Money "USD"
5twoDollars = Money 2

This is all thanks to another GHC extension we have used - DataKinds. When this extension is enabled, the type constructors are promoted to Kinds and value constructors are promoted to type constructors.

In our case, the kind Symbol is already provided by GHC. All we need is for GHC to promote and recognize "GBP" as type.

 1λ> :k Money "GBP"
 2
 3<interactive>:1:7: error:
 4    Illegal type:"GBP"Perhaps you intended to use DataKinds
 5
 6λ> :set -XDataKinds
 7λ> :k Money "GBP"
 8Money "GBP" :: *
 9
10λ> :k Money Int
11
12<interactive>:1:7: error:
13Expected kind ‘Symbol’, but ‘Int’ has kind ‘*14In the first argument ofMoney’, namely ‘Int15      In the typeMoney Int
Info

The promoted types, "GBP" and "USD" in the above example, have no inhabitants.

Also, promoted types are prefixed with a quote ('"GBP" and '"USD") but they can almost always be ignored as the context of their usage makes it clear which one we meant - the type “GBP” or the string value “GBP”.

The Beginning

There is much more to depended types than what we have seen here. This is to show readers that dependent types can be useful in day-to-day programming as well.