Foundations Key idea: A function is any well-defined mapping, not necessarily numerical

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: f(x)=x2f(x) = x^2, 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.

Common Pitfall

“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 ff from a set AA to a set BB is a rule that assigns to every element of AA exactly one element of BB. That’s the whole definition. Nothing in it mentions arithmetic.

f:ABf : A \to B
  • AA is called the domain — the set of allowed inputs.
  • BB 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 BB that actually gets hit by some input. The range is always a subset of the codomain, and sometimes a strictly smaller one.
A function mapping fruit to colours, with green in the codomain but not the range

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 x2x^2 is — arguably a cleaner example, because there’s no temptation to think “function” means “formula you plug a number into.”

Cool Fact

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
Side by side comparison of a total and a partial function

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.

λA Category-Theoretic View

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 \bot). 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:ZN,square(x)=x2\text{square} : \mathbb{Z} \to \mathbb{N}, \qquad \text{square}(x) = x^2
square :: Integer -> Integer
square x = x * x

Here the domain is Z\mathbb{Z} (all integers), the codomain is N\mathbb{N} (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 {0,1,4,9,16,}\{0, 1, 4, 9, 16, \dots\}. Three different sets, three different roles — and not one of them requires you to imagine a graph.