Functions in Haskell
Learn how to define and use functions in Haskell - type signatures, parameters, recursion, currying, higher-order functions, and composition with Docker-ready examples
Functions are not just a feature of Haskell - they are Haskell. As a purely functional language, every Haskell program is built by defining functions and composing them together. There are no statements, no void methods that exist only for their side effects, and no loops in the traditional sense. A function takes input, returns output, and (unless it lives in IO) does nothing else.
This focus changes how you write functions compared to imperative languages. Functions are first-class values: you can pass them as arguments, return them from other functions, and store them in data structures. Every function is curried by default, which makes partial application natural. And because there is no mutable state, repetition is expressed through recursion and higher-order functions like map and filter rather than for and while.
In this tutorial you will learn how to define functions with explicit type signatures, scope local values with where and let, write recursive functions, and work with currying, closures, higher-order functions, and function composition.
Defining Functions
A Haskell function definition has two parts: an optional type signature (name :: types) and one or more equations (name args = body). Arguments are separated by spaces, and the last type after the final -> is the return type.
Create a file named functions.hs:
| |
Notice there is no return keyword: a function’s body is its return value. The where and let constructs both introduce local bindings that are only visible inside that function, keeping helper values out of the global scope. Haskell has no concept of default parameter values - instead, you define multiple functions or use partial application (shown below).
Recursion Instead of Loops
Without mutable variables, Haskell expresses repetition through recursion. Functions are defined by pattern matching on their arguments: list the base cases first, then the recursive case. Guards (|) let you choose an equation based on a boolean condition.
Create a file named recursion.hs:
| |
Each equation handles a specific shape of input. For sumList, the empty list [] is the base case, and (x:xs) destructures a non-empty list into its first element and the rest. This pattern - base case plus a step that shrinks the problem - is the functional replacement for iteration.
Higher-Order Functions and Currying
A higher-order function takes a function as an argument or returns one. Because Haskell functions are curried, a function of “two arguments” is really a function that takes one argument and returns a function expecting the rest - which is what makes partial application so convenient.
Create a file named higher_order.hs:
| |
The built-in map and filter are the workhorses of functional iteration: map applies a function to every element, and filter keeps elements that satisfy a predicate. The composition operator (.) glues functions together so the output of one becomes the input of the next, letting you build complex transformations from small, reusable pieces.
Running with Docker
| |
Expected Output
Running functions.hs:
add 3 4 = 7
square 5 = 25
circleArea 2.0 = 12.566370614359172
hypotenuse 3 4 = 5.0
Running recursion.hs:
factorial 5 = 120
fib 10 = 55
sumList [1..10] = 55
Running higher_order.hs:
applyTwice (+3) 10 = 16
addTen 5 = 15
triple 7 = 21
incThenDouble 4 = 10
map (*2) [1,2,3] = [2,4,6]
filter even [1..10] = [2,4,6,8,10]
Key Concepts
- Type signatures (
name :: A -> B -> C) describe a function’s inputs and output; the type after the last->is the return type. They are optional but strongly recommended for documentation and error-checking. - No
returnstatement - a function’s body is its value. (Thereturnyou may see indoblocks is unrelated; it wraps a value in a monad.) - Pattern matching lets you define a function with multiple equations, one per shape of input, with base cases listed first.
- Guards (
|) select an equation based on boolean conditions, withotherwiseas the catch-all. - Recursion replaces loops - there is no
fororwhile; you shrink the problem toward a base case instead. - Functions are first-class - pass them as arguments, return them, and store them like any other value.
- Currying, partial application, and closures - every multi-argument function can be applied to some of its arguments to produce a new function, and returned functions capture variables from their surrounding scope (as the lambda inside
multipliercapturesn). - Composition with
(.)and helpers likemapandfilterlet you build large behaviors from small, pure functions.
Comments
Loading comments...
Leave a Comment