Making Your Own Data Types in Haskell

So in the last post we looked at some of the built-in types in Haskell. But one of the best parts about Haskell is that it is easy to build our own types. We do this using the “data” keyword, and then giving a type name (which must begin with a capital letter):

data FightMove = …

Type Constructors

We then describe our type with a series of type constructors, separated by the | character. Each of these also has a name that begins with a capital letter. Let’s look at the most basic type constructors we can have:

data FightMove = Punch | Kick | Block

To see these type constructors in action, let’s build a function which takes some input, and returns a FightMove. We can customize which type of move we return based on the input.

chooseMove :: Int -> FightMove
chooseMove i = if i == 1
  then Punch
  else if i == 2
    then Kick
    else Block

Notice how we use the different constructors in the different cases, but they all typecheck as a FightMove! Now you might be wondering at this point, are these type constructors like constructors in, say, Java? In Java, we can have multiple constructors that each take a different set of parameters but all produce the same type of object.

However, no matter what, the resulting object in Java will always have the same instance variables with the same types. This is not the case in Haskell! Different cases of our types can have entirely different fields. For each constructor, we are not just listing the types it is initialized with, we are listing all the fields the object will have.

data FightMove = Punch Int |
  Kick Int Float |
  Block String

chooseMove :: Int -> FightMove
chooseMove i = if i == 1
  then Punch 3
  Else if i == 2
    then Kick 3 6.5
    else Block “Please Protect Me”

So if we’re taking our type as input, how do we determine which type constructor was used? If we think back to the example with Maybe types in the last article, we remember using pattern matching to determine whether a value was Nothing or Just. But Nothing and Just are simply the two type constructors of Maybe! So we can use the same pattern matching approach for our types!

evaluateMove :: FightMove -> String
evaluateMove (Punch damage) = “Punched for “ ++ (show damage) ++ “ damage”
evaluateMove (Kick damage angle) = “Kicked at “ ++ (show angle) ++ “ degrees for “ ++ (show damage) ++ “ damage”
evaluateMove (Block message) = “Blocked while saying “ ++ message

Parameterized Types

Speaking of Maybe, how is it that Maybe can wrap any type? How can we have a Maybe Int type and a Maybe [Float] type? The answer is that types can be “parameterized” by other types! Maybe is implemented as follows:

data Maybe a = Nothing | Just a

The “a” value in this definition is the parameter of the Maybe type. This lets us use any type we want to be wrapped inside! If we wanted our user to choose the way they represent “damage” in the FightMove type, we could easily parameterize this type as well:

data FightMove a = Punch a |
  Kick a Float |
  Block String

Now we can have separate types like FightMove Int, or FightMove Float, just as we can have different Maybe types like Maybe Int or Maybe Float.

Record Syntax

As you get more comfortable creating your own types, you’ll make some that have many different fields. For instance, consider this type, which has one constructor with five fields.

data Person = Person String String Int String String

The only tool we have for accessing these fields right now is pattern matching, like we used above. If we wanted to get the values of these fields out, our code would need to look something like this:

funcOnPerson :: Person -> String
funcOnPerson (Person str1 str2 int1 str3 str4) = …

But what if we only cared about one of these fields? This code seems rather cumbersome for extracting that. On top of that, how are we supposed to keep track of what field represents what value? This is where record syntax comes into play. Record syntax allows us to assign a name to each field of a constructor.

data Person = Person
  { firstName :: String
  , lastName :: String
  , age :: Int
  , hometown :: String
  , homestate :: String }

Now that our different fields have names, each name actually acts as a function allows us to extract that value from the larger object. We no longer have to deconstruct the entire object via pattern matching to get each value out.

birthPlace :: Person -> String
birthPlace person = hometown person ++ “, “ ++ homestate person

It is also easy to create an updated copy of a person using record syntax. The following code will make a new Person that preserves the age, hometown, and home state fields of the original, but has a different name:

makeJohnDoe :: Person -> Person
makeJohnDoe person = person
  { firstName = “John”
  , lastName = “Doe” }

Summary

  1. You can make your own data types, with several different constructors.
  2. Constructors can have different numbers of parameters and different types.
  3. A type can be parameterized by other types, allowing a user to fill in what kind of type will be used inside the object.
  4. Record syntax allows us to access or change* fields of an object.

*Haskell objects are immutable, and cannot actually be changed. When you do this, you create a new copy of the object! We’ll discuss immutability more later!

If you’re super excited about making your own types and want to try out Haskell, check out our free checklist to kickstart your Haskell learning process. And stay tuned for more content here at the Monday Morning Haskell blog!

Previous
Previous

Making Your Types Readable!

Next
Next

Using Compound Types to Make Haskell Easier