Polyglot
Languages Haskell types
Haskell § types

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:

TypeDescription
IntFixed-precision integer (typically 64 bits)
IntegerArbitrary-precision integer
WordFixed-precision unsigned integer
Float, DoubleIEEE 754 binary32 / binary64
CharUnicode character
String= [Char]; a list of characters
BoolTrue or False
()The unit type; one value ()
(a, b), (a, b, c), …Tuples
[a]List of a
Maybe aAn a or nothing
Either a bAn a or a b
IO aAn 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, Monad are 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 itselfIORef Int is a different type from Int; the type system reflects mutability.
  • Implicit conversions — explicit fromIntegral and friends are required.
  • null for any typeMaybe makes 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.