Skip to content
Algebraic Data Types
step 1/7

Reading — step 1 of 7

Learn

~3 min readCustom Types

Algebraic data types are how Haskell models domain data. The data keyword creates new types with sum (alternatives) and product (combinations) shapes. LYAH treats this as foundational; Real World Haskell uses it from chapter 3 onward.

Sum types — alternatives

data Color = Red | Green | Blue

Color is a new type with three constructors — Red, Green, Blue. They take no arguments — just enum-like tags.

favorite :: Color
favorite = Blue

describe :: Color -> String
describe Red   = "warm"
describe Green = "natural"
describe Blue  = "cool"

Pattern matching is exhaustive — the compiler warns if a case is missing.

Constructors with data

data Shape = Circle Double | Square Double | Rectangle Double Double

area :: Shape -> Double
area (Circle r)        = pi * r * r
area (Square s)        = s * s
area (Rectangle w h)   = w * h

Each constructor takes its own arguments. Circle 5 is a Shape; so is Rectangle 3 4. Pattern matching destructures them.

This is a sum of products — Shape is one of three (sum), each with its own fields (product).

Records

When a constructor has many fields, named accessors help:

data Person = Person { name :: String, age :: Int, email :: String }

ada :: Person
ada = Person { name = "Ada", age = 36, email = "[email protected]" }

-- Accessors are auto-generated:
name ada           -- "Ada"
age ada            -- 36

-- Update syntax — produces a new Person:
older = ada { age = 37 }

Record syntax generates name :: Person -> String etc. for free.

Type parameters (generics)

data Maybe a = Nothing | Just a

The a is a TYPE PARAMETER. Maybe Int is one type, Maybe String is another. Pattern match:

safeDivide :: Int -> Int -> Maybe Int
safeDivide _ 0 = Nothing
safeDivide a b = Just (a `div` b)

case safeDivide 10 0 of
    Just n  -> putStrLn ("got " ++ show n)
    Nothing -> putStrLn "divide by zero"

Maybe is built-in; this is just showing how it would be defined.

Recursive types

data Tree a = Leaf | Node a (Tree a) (Tree a)

depth :: Tree a -> Int
depth Leaf = 0
depth (Node _ l r) = 1 + max (depth l) (depth r)

A Tree of a is either a Leaf or a Node with a value and two subtrees. Recursive — the (Tree a) references itself.

Deriving — automatic instances

data Point = Point Int Int deriving (Show, Eq, Ord)

deriving auto-generates instances for common type classes:

  • Show — printable (show p)
  • Eq — equality (==, /=)
  • Ord — ordering (<, >)
  • Read — parseable from String
  • Enum — successor/predecessor
  • Bounded — minBound/maxBound

Without deriving Show, print p won't compile. Almost always include Show, Eq at minimum.

When to use ADTs

  • Domain modeling: data Status = Active | Pending | Archived
  • Result types: data Result a = Ok a | Err String
  • AST nodes: data Expr = Lit Int | Add Expr Expr | Mul Expr Expr
  • State machines: data Order = Cart | Confirmed | Shipped | Delivered

ADTs + pattern matching = type-safe state machines. Add a new variant; the compiler tells you every match site that needs updating.

Common mistakes

  • Forgetting deriving Showprint and many other things won't compile.
  • Constructor name shadows type namedata Foo = Foo Int is fine but confusing initially. Constructor and type can share names.
  • Capitalization — type names AND constructor names are uppercase. Type variables (the a in Maybe a) are lowercase.
  • Mixing Maybe with NothingNothing is the constructor; Maybe is the type. Use Maybe Int -> Maybe Int in signatures.
  • Plain enums when records would clarify — for things with multiple fields, records are more maintainable than positional constructors.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…