What IS a Function?
The single most common beginner mistake: assuming functions are about numbers. They're about mappings — from anything, to anything.
The first mistake
Almost everyone arrives at “function” already carrying a picture from school: , a graph, a curve on graph paper. That picture isn’t wrong, but it’s a single special case wearing the whole concept’s name — and it quietly teaches the wrong lesson, which is that a function is fundamentally about numbers.
“A function takes a number and returns a number.” This is the single most common assumption that trips up beginners moving from math class into Functional Programming — and it’s simply false. A function is a mapping between any two sets. The inputs and outputs can be numbers, but they can just as easily be fruit, colours, people, files, or other functions.
The actual definition
A function from a set to a set is a rule that assigns to every element of exactly one element of . That’s the whole definition. Nothing in it mentions arithmetic.
- is called the domain — the set of allowed inputs.
- is called the codomain — the set the outputs are drawn from (not necessarily all of it — see below).
- The range (or image) is the subset of that actually gets hit by some input. The range is always a subset of the codomain, and sometimes a strictly smaller one.
Figure: colourOf :: Fruit -> Colour. The domain is a set of fruit, the codomain a set of colours — and notice green sits in the codomain but is never actually produced, so it isn’t in the range.
In Haskell, this is written directly as a type signature:
data Fruit = Apple | Banana | Grape
data Colour = Red | Yellow | Purple | Green
colourOf :: Fruit -> Colour
colourOf Apple = Red
colourOf Banana = Yellow
colourOf Grape = Purple
No numbers appear anywhere. colourOf is exactly as much a function as is — arguably a cleaner example, because there’s no temptation to think “function” means “formula you plug a number into.”
The word “function” itself comes from Leibniz, who used it around 1673 for a quantity that depends on a curve. It took another 150+ years — through Euler, Dirichlet, and eventually set theory — for the definition to loosen into the fully general “any mapping between sets” version Haskell inherits today.
Total functions vs. partial functions
Look again at the definition: every element of the domain must map to exactly one output. A function that satisfies this for its entire stated domain is called total. A function that doesn’t — one that’s undefined for some inputs — is called partial.
-- total: every list, including [], produces an answer
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:_) = Just x
-- partial: crashes on []; not actually a function over ALL of [a]
head :: [a] -> a
head (x:_) = x
-- head [] ⇒ *** Exception: Prelude.head: empty list
Figure: head' handles every possible NonEmpty a, so it’s total on its stated domain. The library head is only defined for non-empty lists, but its type claims to accept [a] — including [] — which is where the partiality hides.
This distinction matters enormously in Haskell, because the type signature is a promise. head :: [a] -> a promises an a for any [a] you hand it — a promise the empty list breaks at runtime. safeHead :: [a] -> Maybe a makes an honest, keepable promise instead: “you’ll get something, but that something might be ‘nothing.’” Preferring total functions — functions whose types tell the truth about every input — is one of the most valuable habits Haskell teaches.
In category-theoretic terms, an ordinary Haskell function a -> b is already total by convention (ignoring undefined and infinite loops, which category theorists politely call “bottom,” written ). A genuinely partial function is instead usually modeled as a total function into Maybe b — turning “sometimes has no answer” into an honest value, Nothing, that lives in the codomain. This is the first hint of a pattern this book returns to again and again: Haskell tends to make partiality, failure, and effects into values, rather than exceptions to the rules.
Functions are values too
One more habit worth building early: in Haskell, a function is not a special kind of thing separate from “data” — it’s a value like any other, and can be passed around, stored, and returned from other functions.
apply :: (a -> b) -> a -> b
apply f x = f x
twice :: (a -> a) -> a -> a
twice f = f . f
-- colourOf itself, unapplied, is a perfectly ordinary value:
lookupTable :: [Fruit -> Colour]
lookupTable = [colourOf, colourOf]
This is what people mean by “functions are first-class” — and it’s the property that makes everything from Chapter 9 onward (fmap, <*>, >>=) possible: they’re just ordinary functions that happen to take other functions as arguments.
Try it yourself
square :: Integer -> Integer
square x = x * x
Here the domain is (all integers), the codomain is (non-negative integers — Haskell’s Integer doesn’t enforce this, but mathematically that’s the honest codomain), and the range is the even smaller set of perfect squares . Three different sets, three different roles — and not one of them requires you to imagine a graph.