coldwa.st
All guidesProgrammingWebDataToolsDatabasesHaskellConceptsCabal & buildsToolchainCompilerPerformanceEditor & HLS

Haskell · types · fundamentals

How to read a Haskell type signature

By ColdwastUpdated Aug 12, 20268 min read#haskell#types#beginners
A blue-toned architectural drawing photographed at a shallow angle, its cover sheet lettered in white with the Russian words for architectural drawings above a dimensioned floor plan
A blue architectural sheet shot at a shallow angle. White lettering across the top reads СТРОИТЕЛЬНЫЕ ЧЕРТЕЖИ, with ARCHITECTURAL DRAWINGS beneath it; below, a floor plan covered in dimension figures falls out of focus toward the bottom edge.

Haskell signatures are the part beginners skip, and they are the part that pays the most to learn. A signature is not documentation attached to a function - it is a near-complete specification of what the function can do. Once you can read one, you can often guess what a function does without reading a line of its implementation.

The apparent difficulty comes from three conventions that nobody states explicitly. Learn those and the rest is reading left to right.

Rule 1 - the arrow groups to the right

Start with the simplest case:

not :: Bool -> Bool

Read it as "not takes a Bool and gives back a Bool". The :: means "has type". So far so obvious. Now two arguments:

replicate :: Int -> a -> [a]

The temptation is to read this as "takes an Int and an a, returns a list". That reading works in practice, but it hides what is really going on, and the real structure explains several things that otherwise look arbitrary. The arrow associates to the right, so the compiler sees:

replicate :: Int -> (a -> [a])

Every Haskell function takes exactly one argument. replicate takes an Int and returns another function, which takes an a and returns a list. This is currying, and it is why partial application needs no special syntax: replicate 3 is simply the inner function, perfectly usable on its own.

The practical rule: the last thing after the last arrow is the return type; everything before is an argument - as long as no parentheses say otherwise.

Rule 2 - lowercase means "any type"

Case is significant, and it carries a lot of meaning:

  • Uppercase - a concrete type. Bool, Int, String, Maybe.
  • Lowercase - a type variable: any type at all, chosen by the caller.

The names themselves mean nothing. a -> a and foo -> foo are the same type. What matters is which letters repeat. In a -> a, the two as must be the same type. In a -> b they may differ - but they are not obliged to, since b can be chosen to equal a.

This is where signatures start telling you things. Consider:

mystery :: a -> a

The function must work for every type, so it cannot inspect its argument - it has no idea what it is. It cannot produce a value of type a from nothing either. Ignoring bottom (an exception or an infinite loop), there is exactly one thing this function can be: the identity function. The type alone pins it down. That property is why Haskellers say the types do so much work, and it is what makes searching by signature practical in Hoogle.

A grey metal post carrying nine blank arrow-shaped direction signs in cream, grey-blue, red, green, yellow, dark blue, purple, light blue and teal, pointing left and right against a blue sky with white cloud
Nine arrow-shaped signs bolted around a grey post, each a different colour and every one of them blank, pointing left and right against a partly clouded blue sky. Nothing is written on any of them.

Rule 3 - the fat arrow is not an argument

This one trips up nearly everybody:

elem :: Eq a => a -> [a] -> Bool

The => is not an arrow in the same sense. Everything to its left is a constraint, not a parameter. Read it as: "for any type a that supports equality, elem takes an a and a list of a and returns a Bool."

So elem takes two arguments, not three. The constraint narrows which types the caller may choose - here, only types with an Eq instance. Multiple constraints are grouped in parentheses and separated by commas:

sortOn :: Ord b => (a -> b) -> [a] -> [a]
showBoth :: (Show a, Show b) => a -> b -> String

Constraints are also a strong hint about behaviour. A signature carrying Ord almost certainly compares things; one carrying Show will render them as text.

Parentheses change the meaning entirely

Because the arrow groups rightward, a pair of parentheses on the left is never decoration:

map :: (a -> b) -> [a] -> [b]

The first argument is a function from a to b. Remove the parentheses and you get a -> b -> [a] -> [b], a completely different function taking three arguments. Whenever you see parentheses to the left of an arrow, you are looking at a higher-order function: it takes a function as input.

Now read map as a whole. It takes a function turning as into bs, plus a list of as, and gives a list of bs. The signature already told you it applies the function to each element, and that it cannot reorder, drop or duplicate anything in a way that depends on the values - it does not know what they are.

Reading the common shapes

A handful of patterns cover most of what you will meet:

filter    :: (a -> Bool) -> [a] -> [a]
foldr     :: (a -> b -> b) -> b -> [a] -> b
lookup    :: Eq a => a -> [(a, b)] -> Maybe b
fmap      :: Functor f => (a -> b) -> f a -> f b
putStrLn  :: String -> IO ()
readFile  :: FilePath -> IO String
  • filter - a test on a, a list of a, a list of a. The element type never changes, so it can only keep or drop.
  • lookup - the Maybe b return says failure is possible and is part of the type, not an exception.
  • fmap - the variable f is applied to another type, so it is a type constructor, not a plain type. Read f a as "some container or context holding as".
  • putStrLn - IO () means it performs an effect and returns nothing useful. () is the unit type: exactly one value, carrying no information.

When you cannot tell how many arguments there are

Type synonyms can hide arrows, and this is the one case where reading left to right misleads you:

type Handler = Int -> String

process :: Handler -> Bool

That looks like one argument, and after expansion it is (Int -> String) -> Bool - still one argument, but a function. Expansion the other way is worse: a synonym for a function type on the right of the last arrow silently adds a parameter. When a signature refuses to make sense, expand the synonyms before doubting yourself.

Getting the signature in the first place

You do not have to find these in documentation. In GHCi, :t asks for the type of anything, including a partially applied expression, which is the fastest way to see currying at work:

ghci> :t replicate
replicate :: Int -> a -> [a]
ghci> :t replicate 3
replicate 3 :: a -> [a]

In an editor, Haskell Language Server shows the same information on hover and can insert a missing top-level signature for you. And when you know the shape you want but not the name, the search runs the other way round - that is exactly what Hoogle's type search is for.

A worked example, left to right

traverse :: (Applicative f, Traversable t) => (a -> f b) -> t a -> f (t b)

Take it in order. Everything before => is a constraint: f must be an Applicative, t must be Traversable. First real argument, in parentheses, is a function from a to f b - it produces an effect. Second argument is t a: a structure holding as, a list say. The return is f (t b).

So the effect that was inside each element in the argument has moved outside the structure in the result. A list of things each producing an effect becomes one effect producing a list. You have not read the implementation, and you already know what it does.

FAQ

What does the double colon mean in Haskell? :: is read "has type". x :: Int means x has type Int. Note this is the opposite of many other languages, where :: is a namespace separator.

What is the difference between -> and => in a type signature? The thin arrow separates arguments and the return type. The fat arrow separates constraints from the type itself: everything to the left of => restricts which types the caller may choose, and contributes no arguments.

Why are some type names lowercase? Lowercase names are type variables standing for any type, fixed by the caller. Uppercase names are concrete types or type constructors. Repeating the same lowercase letter forces those positions to be the same type.

How do I know how many arguments a function takes? Count the top-level arrows, ignoring anything inside parentheses and anything left of =>. The final type after the last top-level arrow is the return type. Watch for type synonyms, which can hide arrows on either side.

What does IO () mean? An action that performs input or output and returns the unit value () - that is, no useful result. It is the type of things you run for their effect.

How do I see the type of an expression? Use :t expression in GHCi, or hover over the name in an editor running Haskell Language Server.

Recommended

Somewhere to build when your laptop runs out of memory

GHC and Haskell Language Server are memory-hungry on large projects, and a first full build of a dependency tree is the point where a modest machine starts swapping. Building on a cloud server with more RAM than your laptop is a common way round it. DigitalOcean offers VPS and cloud servers sized for that.

See DigitalOcean cloud →

Affiliate link - supports these free guides.

Independent, community-maintained guide. coldwa.st is a programming-resources site; this article is new, original explanatory writing about Haskell and is not affiliated with the GHC maintainers or the Haskell Foundation. Signatures shown are those of the standard libraries at the time of writing - check the current documentation, as types are occasionally generalised between releases.

Related reading: Hoogle: searching Haskell by type signature · Monads in Haskell · What is Haskell