Making Your Types Readable!

So in the last post we discussed creating our own data types using the data keyword. This is an excellent way to describe most of our data. But there are more ways we should explore. We’ll look at two simpler alternatives to creating an full data type.

The Type Keyword

The first method we’ll discuss is the type keyword. This keyword creates a type synonym, much like the typedef keyword from C. We take some type, and simply give it a new name we can use to reference it.

type MyWrapperType = (String, Int, Float)

When we do this, the code’s behavior does not really change. It makes no difference whether we call the type by its new name or its old name. We can even use both in the same function! All the same pattern matching rules apply to the synonym as the original type.

myTypeFunction :: MyWrapperType -> (String, Int, Float) -> MyWrapperType
myTypeFunction (str1, int1, flt1) (str2, int2, flt2) = (str2, int1, flt1)

The type keyword is an excellent way to clarify compound types. As you write more complicated code, you’ll find you often need to have more complicated types, like nested tuples and lists. For example, suppose some function generates a 5-tuple, and a few other functions take the type as input.

complicatedFunction :: … -> ([Int], Map String Float, Maybe String, Float, Int)

func1 :: ([Int], Map String Float, Maybe String, Float, Int) -> …

func2 :: ([Int], Map String Float, Maybe String, Float, Int) -> …

func3 :: ([Int], Map String Float, Maybe String, Float, Int) -> ...

We can dramatically clarify this code with type:

type UserChunk = ([Int], Map String Float, Maybe String, Float, Int)

complicatedFunction :: … -> UserChunk

func1 :: UserChunk -> …

func2 :: UserChunk -> …

func3 :: UserChunk -> …

The best example of the type synonym in normal Haskell is the String type. String is actually just a type name for a list of characters!

type String = [Char]

One possible downside to using type synonyms is the compiler may not use your synonym when telling you about errors. Thus you need to be aware of the different renamed types in your code as you analyze errors.

Newtypes

Now we’ll discuss a different way of expressing our types: the newtype keyword. This creates a new, unique type, wrapping a single other type. For instance, we could create an Interest Rate newtype wrapping a floating point number:

newtype InterestRate = InterestRate Float

The syntax for newtype is just like creating an algebraic data type, except it can only use one constructor, and that constructor must have exactly one parameter. You can still use record syntax with a newtype. One convention is to give the field a name such as “un”-(newtype name), as follows:

newtype InterestRate = InterestRate { unInterestRate :: Float }

As with ADTs, record syntax allows you to unwrap the original float value without pattern matching. Though you can do either if you want:

func1 :: InterestRate -> Float
func1 (InterestRate flt1) = flt1 + 2.5

func2 :: InterestRate -> Float
func2 intRt = (unInterestRate intRt) + 2.5

The most important point about newtypes is they DO change compile time behavior! The following code would not work:

rate :: InterestRate
rate = InterestRate 0.5

badCall :: Float
badCall = func1 rate

func1 :: Float -> Float
func1 flt1 = flt1 + 2.5

This is because func1 demands a Float as input, but badCall passes an InterestRate, which is a different type. This is super helpful when you have numerous items which have the same underlying type, but actually represent different kinds of values.

Using Newtypes to Catch Errors

Let’s see how we can use newtypes to make a real program safer. Suppose you’re writing code for a bank. You might write a function taking a couple interest rates and a bank balance and performing some operations on them. If these are all represented by floating point numbers, you might get code looking like:

bankFunction :: Float -> Float -> Float -> Float
bankFunction balance intRate fedRate = balance * (1 + intRate + fedRate)

But now imagine trying to call this function. There are numerous ways to misorder the arguments, and the program will still compile without telling you about the error!

determineNewBalance :: Float -> Float
-- Wrong order!
determineNewBalance balance = bankFunction intRate fedRate balance
  Where
    intRate = 6.2
    fedRate = 0.5

This code will have an error you can only catch at runtime (or with good testing), since we misordered the different float arguments. We want to avoid such code in Haskell when we can, because Haskell gives us the tools to do so! Let’s see how newtypes can help us avoid this error:

newtype BankBalance = BankBalance { unBankBalance :: Float }
newtype InterestRate = InterestRate { unInterestRate :: Float }

bankFunction :: BankBalance -> InterestRate -> InterestRate -> BankBalance
bankFunction (BankBalance b) (InterestRate i) (InterestRate f) = BankBalance (b * (1 + i + f))

determineNewBalance :: BankBalance -> BankBalance
determineNewBalance b = bankFunction intRate fedRate b
  where 
    intRate = InterestRate 6.2
    fedRate = InterestRate 0.5

This code, which contains the same fundamental error, will not compile anymore! This is because we pass interest rates as the first two parameters , when the first parameter to bankFunction is actually a BankBalance. Marvelous! Note, using type synonyms would not have helped us here, because the underlying types would continue to be floats.

Summary

  1. The type keyword allows us to create type synonyms.
  2. It can allow renaming of complex types without altering compiler behavior.
  3. The newtype keyword allows us to create a wrapper around a single type.
  4. This makes it easier for us to distinguish (at compile time) between variables which have the same underlying type, but different meanings in our code.

Now that you’re ready to use types and newtypes to make your Haskell awesome, time to start making a Haskell project! Check out this checklist to make sure you have all the tools to get going!

Previous
Previous

Many Types, One Behavior

Next
Next

Making Your Own Data Types in Haskell