Types
Haskell’s type system is the language’s most distinctive feature. The base is the Hindley-Milner type system: every expression has a unique most-general type, the compiler infers types without programmer annotation, and ill-typed programs are rejected categorically. Haskell extends Hindley-Milner with type classes (ad-hoc polymorphism), algebraic data types (the principal data-modelling mechanism), and a substantial set of optional extensions (type families, GADTs, rank-N types) that admit advanced typing patterns. The combination produces a type system substantially more expressive than mainstream alternatives, with corresponding obligations on the programmer to understand it.
This page covers the type-system surface a working programmer encounters; the dedicated pages cover Type classes, Algebraic data types, and Polymorphism.
Type signatures
A type signature declares the type of a value or function:
n :: Int
n = 42
greet :: String -> String
greet name = "Hello, " ++ name
add :: Int -> Int -> Int
add x y = x + y
The :: reads “has type”. String -> String is the type of a function from String to String; Int -> Int -> Int reads as Int -> (Int -> Int) — a function that takes an Int and returns a function from Int to Int. (Function types are right-associative; this is currying.)
Type signatures are optional — the compiler infers types — but the conventional discipline is to write them for every top-level function. Explicit signatures:
- Document the function’s contract.
- Catch errors earlier (the compiler verifies the inferred type matches).
- Pin down the intended type when the inferred type would be more general than appropriate.
Built-in types
The principal built-in types:
| Type | Description |
|---|---|
Int | Fixed-precision integer (typically 64 bits) |
Integer | Arbitrary-precision integer |
Word | Fixed-precision unsigned integer |
Float, Double | IEEE 754 binary32 / binary64 |
Char | Unicode character |
String | = [Char]; a list of characters |
Bool | True or False |
() | The unit type; one value () |
(a, b), (a, b, c), … | Tuples |
[a] | List of a |
Maybe a | An a or nothing |
Either a b | An a or a b |
IO a | An IO action producing a |
Numeric literals are polymorphic: 42 has type Num a => a, the type of any Num instance. The actual type is fixed by context:
n1 = 42 :: Int
n2 = 42 :: Double
n3 = 42 -- inferred from context
Floating-point literals (with a .) are similarly polymorphic with Fractional a => constraint.
Function types
A function type is written with ->:
add :: Int -> Int -> Int -- two-argument function
length :: [a] -> Int -- polymorphic; one type variable
const :: a -> b -> a -- two type variables
The arrow is right-associative: a -> b -> c is a -> (b -> c). This admits currying — every function is conceptually a one-argument function that may return another function:
add :: Int -> Int -> Int
add x y = x + y
addOne :: Int -> Int
addOne = add 1 -- partial application; addOne is `\y -> add 1 y`
The treatment of currying and higher-order functions is in Functions.
Type variables
A type variable is a lowercase identifier that stands in for any type:
identity :: a -> a
identity x = x
const :: a -> b -> a
const x _ = x
length :: [a] -> Int
length [] = 0
length (_:xs) = 1 + length xs
The variable a (or b, c, etc.) ranges over all types; the type signature is parametrically polymorphic. The compiler instantiates the variable at each call site:
identity 5 -- a = Int
identity "hello" -- a = String
identity True -- a = Bool
The type variable is universally quantified — forall a. a -> a — though the forall is implicit in standard Haskell.
The full treatment of polymorphism is in Polymorphism.
Type class constraints
A type may be constrained to belong to a type class:
sum :: Num a => [a] -> a
elem :: Eq a => a -> [a] -> Bool
sort :: Ord a => [a] -> [a]
show :: Show a => a -> String
The Num a => constraint reads “for any type a that is an instance of Num”. The constraint enables the function body to use +, -, *, 0 (members of the Num class) on values of type a.
Multiple constraints are written as a tuple:
process :: (Eq a, Show a) => a -> a -> String
The treatment of type classes is in Type classes.
Algebraic data types
A data declaration introduces a new type:
-- Sum type:
data Direction = North | East | South | West
-- Product type:
data Point = Point Double Double
-- Sum + product:
data Shape = Circle Double | Square Double | Rectangle Double Double
-- Recursive:
data List a = Nil | Cons a (List a)
-- Records:
data Person = Person
{ name :: String
, age :: Int
}
The data keyword introduces the type; the = separates the type from its constructors; the | separates alternative constructors. Each constructor takes zero or more arguments and produces a value of the type.
Constructors may be used in expressions (to construct values) and in patterns (to deconstruct):
origin :: Point
origin = Point 0 0
distance :: Point -> Point -> Double
distance (Point x1 y1) (Point x2 y2) =
sqrt ((x2 - x1) ** 2 + (y2 - y1) ** 2)
The full treatment is in Algebraic data types.
Type aliases (type)
The type keyword introduces an alias — a synonym for an existing type:
type Name = String
type Age = Int
type Point = (Double, Double)
type Predicate a = a -> Bool
A type alias is interchangeable with the underlying type; the compiler treats them as the same. Aliases are documentation aids; they do not introduce new types.
For new types with the same underlying representation, use newtype:
newtype Age = Age Int
birthAge :: Age
birthAge = Age 0
-- Age and Int are now distinct types:
-- birthAge + 1 -- compile error: Age is not Int
newtype declarations admit a single constructor with a single argument and produce a zero-cost wrapper — the runtime representation is identical to the wrapped type, but the type system distinguishes them. The mechanism is the conventional way to add type-level distinctions without runtime overhead.
Records
A record syntax admits named fields:
data Person = Person
{ personName :: String
, personAge :: Int
, personEmail :: Maybe String
}
The fields are accessor functions:
alice :: Person
alice = Person { personName = "Alice", personAge = 30, personEmail = Nothing }
n :: String
n = personName alice -- "Alice"
Record-update syntax produces a new record with selected fields modified:
older :: Person
older = alice { personAge = 31 }
Records have several historical pitfalls — field-name collisions across types, the awkward update syntax — that recent extensions (DuplicateRecordFields, OverloadedRecordDot, RecordWildCards) have addressed. The treatment is in Algebraic data types.
Conversions
Haskell does not perform implicit numeric conversions. Conversions between types must be explicit:
fromIntegral :: (Integral a, Num b) => a -> b
realToFrac :: (Real a, Fractional b) => a -> b
toRational :: Real a => a -> Rational
fromRational :: Fractional a => Rational -> a
n :: Int
n = 42
d :: Double
d = fromIntegral n -- explicit conversion
The fromIntegral function is the conventional integer-to-other-numeric conversion; the type-class constraints admit converting Int to Double, Integer to Word, etc.
The lack of implicit conversion is a discipline aid: every numeric-type change is visible in the source. The trade-off is verbosity for code that mixes numeric types frequently; the verbosity is conventionally accepted.
The Maybe and Either types
Two essential types in base:
data Maybe a = Nothing | Just a
data Either a b = Left a | Right b
Maybe a represents “an a or nothing”; the conventional return type for fallible operations:
lookup :: Eq k => k -> [(k, v)] -> Maybe v
lookup _ [] = Nothing
lookup k ((k', v) : rest)
| k == k' = Just v
| otherwise = lookup k rest
Either a b represents “a value of type a or a value of type b”; the conventional return type for value-or-error:
parse :: String -> Either String Int
parse s = case reads s of
[(n, "")] -> Right n
_ -> Left ("could not parse: " ++ s)
By convention, Left is the error case and Right is the success case (mnemonic: right is correct).
The two types are the conventional Haskell idiom for representing absence and failure without exceptions.
Tuples
A tuple is an anonymous fixed-arity record:
pair :: (Int, String)
pair = (42, "hello")
triple :: (Int, String, Bool)
triple = (1, "ok", True)
unit :: ()
unit = ()
Tuples admit pattern-matching destructuring:
(n, s) = pair
n :: Int
s :: String
The fst and snd functions extract the first and second components of a 2-tuple:
fst :: (a, b) -> a
snd :: (a, b) -> b
For larger tuples, pattern matching is the conventional access mechanism. Records are preferable for non-trivial product types.
Lists
The list type [a] is the principal sequence type in Haskell. It is a singly-linked list; the operations are:
[] -- empty list
1 : [2, 3, 4] -- 1 prepended (the cons operator)
[1, 2, 3, 4] -- list literal
[1..10] -- range
[1, 3..10] -- with step
[x * 2 | x <- [1..5]] -- list comprehension
The list type is recursive: [a] is conceptually data [a] = [] | a : [a]. The constructors are the empty list [] and the cons constructor :. Pattern matching against a list:
length :: [a] -> Int
length [] = 0
length (_:xs) = 1 + length xs
The _:xs pattern matches a non-empty list and binds xs to the tail.
The full treatment of lists and related collections is in Data structures.
Higher-kinded types
Type-level functions — types that take type arguments — appear throughout Haskell. The list type, for example, is [], which takes one type argument:
[] :: * -> *
The kind * -> * reads “a type-level function from a type to a type”. The * (or Type) is the kind of types that have values. Higher-kinded types appear in:
- Constructors:
[],Maybe,IO,Either. - Type classes that abstract them:
Functor,Applicative,Monadare constraints on* -> *types.
class Functor f where
fmap :: (a -> b) -> f a -> f b
The f here is a type variable of kind * -> *. The treatment is in Polymorphism and Functors and applicatives.
The IO type
The IO type marks effectful computation:
main :: IO () -- a top-level IO action
putStrLn :: String -> IO () -- print a string and a newline
getLine :: IO String -- read a line
readFile :: FilePath -> IO String -- read a file
A value of type IO a is a description of an effectful computation that, when run, produces an a. The type system tracks the IO-ness explicitly: IO String is distinct from String, and pure code cannot perform I/O without using the IO monad.
The full treatment is in IO.
Type inference
Haskell’s compiler infers the most general type for every binding without programmer annotation:
double x = x + x -- inferred: Num a => a -> a
triple = double . (* 3) -- inferred: Num a => a -> a (using composition)
main = do
line <- getLine
putStrLn (greet line) -- inferred: line :: String
The inference is based on the Hindley-Milner algorithm extended for type classes. The compiler picks the most general type that is consistent with the body’s use of the value; explicit annotations may pin down a less-general type if appropriate.
The principal practical guidance:
- Write type signatures for top-level functions; the compiler verifies them.
- Skip signatures for local helpers; the inferred type is fine.
- When inference produces an unexpected type, the explicit signature reveals the disagreement.
Type inference interacts with extensions (in particular, with RankNTypes and GADTs) in subtle ways; the conventional advice is to write more signatures in code that uses substantial extension surface.
A note on what Haskell types are
Several notable absences from Haskell’s type system:
- Subtyping — there is no class hierarchy; type classes admit ad-hoc polymorphism through constraints rather than inheritance.
- Mutable references in the type itself —
IORef Intis a different type fromInt; the type system reflects mutability. - Implicit conversions — explicit
fromIntegraland friends are required. nullfor any type —Maybemakes absence explicit.
The combination — pure values, explicit effects, parametric and ad-hoc polymorphism, no nulls — is the substance of Haskell’s typing discipline. Reading non-trivial Haskell requires fluency with each.