Skip to content

0009: Email Tutorials Haskell For Beginners ‐ Total vs Partial Functions in Haskell

Bernard Sibanda edited this page Dec 9, 2025 · 2 revisions

LigerLearn Video 9

📑 Table of Contents

  • 9.1. Functions as Mappings Between Types
  • 9.2. What Makes a Function Partial?
  • 9.3. head and last: Classic Partial Functions
  • 9.4. Why Partial Functions Are Dangerous in Practice
  • 9.5. Total Functions: The Ideal You Should Aim For
  • 9.6. Designing Your Own Functions to Be Total
  • 9.7. Glossary of Terms for This Lesson

9.1. Functions as Mappings Between Types

In Haskell, a function is understood as a mapping from values of an input type to values of a return type. This relationship is described in the function’s type signature. For example, the factorial function you may have seen earlier can have a type like:

factorial :: Int -> Int

This says: “factorial takes an Int as input and returns an Int as output.” If you look only at the type signature, it appears to promise that for every Int you give it, you will get an Int back. The type by itself does not say that some integers are “off-limits” or will cause the function to fail. This is the mental model you should start with: a function is supposed to give you a valid result for any value of its input type.

9.2. What Makes a Function Partial?

A partial function breaks that promise. Formally, a partial function is one that does not produce a valid result for some legitimate input of its declared input type. The type signature may claim it works for all values of that type, but in reality there are inputs for which the function will fail—often with an exception or crash at runtime.

In Haskell, the Prelude (the standard default library) contains a few well-known partial functions. Many of these are related to operations on lists. Although they are instructive when learning, they can be problematic in real code if used carelessly.

9.3. head and last: Classic Partial Functions

A classic example is the function head, which returns the first element of a list. Conceptually:

  • head [1, 2, 3] gives 1.
  • head ["hello", "goodbye"] gives "hello".

So far, everything works exactly as expected. The problem arises when you try to take the head of an empty list:

head []

For this input, the program does not quietly return a value; instead, it crashes with a runtime exception such as:

*** Exception: empty list

This happens because head is not defined for empty lists. Its implementation assumes there is at least one element. In other words, head is a partial function: it only works for a subset of the possible inputs of type [a].

The same issue appears with last, which returns the final element of a list. For a non-empty list, last behaves as you’d expect. But for an empty list, last [] also crashes. Just like head, last is partial because it does not handle every list of the input type [a].

9.4. Why Partial Functions Are Dangerous in Practice

The main problem with partial functions is that they can cause runtime errors in situations where the type signature alone suggests everything is safe. For example, the type of head:

head :: [a] -> a

looks perfectly total: it claims to accept any list of a and return an a. But the implementation only works correctly for non-empty lists. The type does not encode that precondition, so the responsibility is entirely on the programmer to avoid invalid inputs.

In real programs, especially larger ones, it can be surprisingly easy to accidentally call a partial function with a “bad” input—like an empty list—far away from where the list was originally created. This leads to crashes that may be hard to track down. For this reason, many Haskell programmers consider the presence of partial functions like head and last in the Prelude to be a historical mistake, even though they are still widely known and used for teaching.

9.5. Total Functions: The Ideal You Should Aim For

In contrast, a total function is one that is defined for every possible input of its declared input type. That means:

  • It does not crash or throw exceptions for valid inputs.
  • For every input value of the type, there is a corresponding valid output value.

The lesson encourages you to treat total functions as the standard you aim for in your own code. A total function honors the promise suggested by its type signature: if it says it works on Int, it truly handles all valid Int values. If it says it works on [a], it handles empty lists as well as non-empty ones.

Total functions make your programs safer, easier to reason about, and more predictable. Since you know they work for all inputs of the given type, you do not need to weave fragile assumptions throughout your code.

9.6. Designing Your Own Functions to Be Total

The guidance from the lesson is simple but powerful:

“Functions that you define should not be partial.”

Instead, aim to write them so that they deal gracefully with every possible input of their input type. That might mean:

  • Handling special cases explicitly (such as empty lists).
  • Changing your types so they more accurately represent valid data.
  • Using safer alternatives (which you will learn about later) to model operations that might fail, instead of crashing.

Although the Prelude includes a few partial functions—mainly for historical and educational reasons—there are not many of them, and you will eventually learn safer, total alternatives (for example, functions that return an explicit “maybe” value instead of throwing exceptions). For now, understanding the concept of total vs partial functions and recognizing why partial functions are risky is an important step in thinking like a functional programmer.

9.7. Glossary of Terms for This Lesson

  • Function A mapping from values of an input type to values of a return type. In Haskell, every function has a type signature describing this mapping.

  • Type signature A declaration that specifies what type(s) a function takes as inputs and what type it returns, typically written with :: and arrows, e.g. f :: A -> B.

  • Total function A function that is defined for every possible input of its input type. It never “blows up” for any valid input value.

  • Partial function A function that fails to produce a result for some valid input of its input type, often by throwing a runtime exception or causing the program to crash.

  • Runtime exception / crash An error that occurs while the program is running (rather than at compile time), often leading to termination of the program, such as calling head [].

  • Prelude The standard module that is automatically imported into every Haskell program. It defines many commonly used types and functions, including a few partial ones.

  • head A Prelude function that returns the first element of a list. It is partial because calling head [] results in a runtime exception.

  • last A Prelude function that returns the last element of a list. It is also partial, failing on an empty list.

  • Input type / return type The types appearing before and after the final arrow in a function type signature. For example, in Int -> Int, the input type is Int and the return type is Int.

Quizz & Progress Badge NFT

Clone this wiki locally