Reading — step 1 of 7
Learn
Haskell has THREE ways to introduce a name for a type. Each has different semantics. The Haskell wiki and Real World Haskell cover them; modern code uses all three.
type — synonyms
type Name = String
type Age = Int
type Email = String
greet :: Name -> String
greet n = "Hello, " ++ n
type creates an ALIAS — Name is just String. Same runtime, same operations. Just a documentation convenience.
fn :: String -> Int -- equivalent to:
fn :: Name -> Age
-- Both compile and are interchangeable.
Useful for documentation but no type safety — you can pass a String where Name is expected because they're the SAME TYPE.
data — new type with constructors
data Email = Email String -- Email is a new TYPE; Email (the constructor) wraps a String
greet :: Email -> String
greet (Email addr) = "Email: " ++ addr
x = Email "[email protected]"
-- y = "[email protected]" :: Email -- error: not an Email
data creates a TRULY NEW type. The compiler enforces the distinction.
newtype — single-constructor data with no overhead
newtype Email = Email { unEmail :: String }
newtype Age = Age Int
newtype is like data BUT:
- Restricted to ONE constructor with ONE field
- The wrapper is ERASED at runtime — same memory layout as the underlying type
- Provides type safety with zero runtime cost
greet :: Email -> String
greet (Email addr) = "Email: " ++ addr
-- compile-time distinction:
greet "plain string" -- ERROR
greet (Email "[email protected]") -- OK
When to use which
| Need | Use |
|---|---|
| Documentation, no enforcement | type |
| Genuine new type with multiple constructors or multiple fields | data |
| Type-safe wrapper around one existing type, zero runtime cost | newtype |
Examples:
type Name = String— just a hintdata Tree a = Leaf | Node a (Tree a) (Tree a)— many constructorsnewtype UserId = UserId Int— distinguish from Int, no overhead
Why newtype matters in practice
newtype Pixel = Pixel Int
newtype Inch = Inch Int
addPixels :: Pixel -> Pixel -> Pixel
addPixels (Pixel a) (Pixel b) = Pixel (a + b)
-- Now mixing units is a type error:
let p = Pixel 100
let i = Inch 5
-- addPixels p i -- TYPE ERROR
addPixels p (Pixel 50) -- OK
Classic "units of measure" pattern. The Pixel and Inch newtypes prevent mixing — the compiler catches Mars Climate Orbiter-style bugs at compile time.
Popular libraries like tagged and refined build on this idea for typed validation.
newtype with deriving
newtype Email = Email String
deriving (Show, Eq, Ord)
newtype Age = Age Int
deriving (Show, Eq, Ord, Num) -- can derive Num via GeneralizedNewtypeDeriving
The Num derive (with the GeneralizedNewtypeDeriving extension) lets Age behave numerically — Age 5 + Age 3 = Age 8 — while staying distinct from Int.
Deriving via and stock
Modern Haskell has more deriving strategies (GHC 8.2+):
newtype EmailNT = EmailNT String
deriving stock (Show, Eq) -- stock: built-in derivation
deriving newtype (Ord) -- newtype: reuse the wrapped type's instance
-- deriving via Validated EmailFormat (Validate) -- via: use a different type's instance
For advanced cases. Day-to-day code uses plain deriving.
Common mistakes
- Reaching for
datawhennewtypefits — newtype is faster (no runtime overhead) and safer. - Reaching for
typewhen you want type safety — type is just a synonym; doesn't prevent mixing. - Trying to use
newtypewith multiple fields or constructors — must usedata. - Using newtype-wrapped types in error messages — they appear with the wrapper.
Pixel 100not100. Sometimes desired, sometimes annoying. - Forgetting accessor names —
newtype Email = Email { unEmail :: String }gives youunEmail :: Email -> String. Without the accessor, you must pattern match.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…