Intermediate

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
module Main where

-- A function of two arguments. The type reads:
-- "add takes an Int and an Int, and returns an Int"
add :: Int -> Int -> Int
add x y = x + y

-- A function of one argument
square :: Int -> Int
square x = x * x

-- Local bindings with 'where' (defined after the body)
circleArea :: Double -> Double
circleArea r = pi * rSquared
  where
    rSquared = r * r

-- Local bindings with 'let ... in' (defined before the body)
hypotenuse :: Double -> Double -> Double
hypotenuse a b =
    let aSquared = a * a
        bSquared = b * b
    in sqrt (aSquared + bSquared)

main :: IO ()
main = do
    putStrLn ("add 3 4         = " ++ show (add 3 4))
    putStrLn ("square 5        = " ++ show (square 5))
    putStrLn ("circleArea 2.0  = " ++ show (circleArea 2.0))
    putStrLn ("hypotenuse 3 4  = " ++ show (hypotenuse 3.0 4.0))

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module Main where

-- Classic recursion via pattern matching on the argument
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)

-- Recursion with guards (the 'otherwise' case is just True)
fib :: Int -> Integer
fib n
    | n < 2     = fromIntegral n
    | otherwise = fib (n - 1) + fib (n - 2)

-- Recursion over a list replaces a 'for' loop.
-- (x:xs) matches the head x and the tail xs.
sumList :: [Int] -> Int
sumList []     = 0
sumList (x:xs) = x + sumList xs

main :: IO ()
main = do
    putStrLn ("factorial 5      = " ++ show (factorial 5))
    putStrLn ("fib 10           = " ++ show (fib 10))
    putStrLn ("sumList [1..10]  = " ++ show (sumList [1..10]))

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
module Main where

-- Takes a function and applies it twice
applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f x)

-- A three-argument function...
addThree :: Int -> Int -> Int -> Int
addThree x y z = x + y + z

-- ...partially applied with two arguments fixed
addTen :: Int -> Int
addTen = addThree 10 0

-- Returning a function. The lambda captures 'n' in a closure.
multiplier :: Int -> (Int -> Int)
multiplier n = \x -> x * n

-- Function composition with (.): apply (+1), then (*2)
incThenDouble :: Int -> Int
incThenDouble = (* 2) . (+ 1)

main :: IO ()
main = do
    putStrLn ("applyTwice (+3) 10  = " ++ show (applyTwice (+ 3) 10))
    putStrLn ("addTen 5            = " ++ show (addTen 5))
    let triple = multiplier 3
    putStrLn ("triple 7            = " ++ show (triple 7))
    putStrLn ("incThenDouble 4     = " ++ show (incThenDouble 4))
    putStrLn ("map (*2) [1,2,3]    = " ++ show (map (* 2) [1, 2, 3]))
    putStrLn ("filter even [1..10] = " ++ show (filter even [1 .. 10]))

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pull the official Haskell image
docker pull haskell:9.6

# Run the basic functions example
docker run --rm -v $(pwd):/app -w /app haskell:9.6 runghc functions.hs

# Run the recursion example
docker run --rm -v $(pwd):/app -w /app haskell:9.6 runghc recursion.hs

# Run the higher-order functions example
docker run --rm -v $(pwd):/app -w /app haskell:9.6 runghc higher_order.hs

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 return statement - a function’s body is its value. (The return you may see in do blocks 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, with otherwise as the catch-all.
  • Recursion replaces loops - there is no for or while; 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 multiplier captures n).
  • Composition with (.) and helpers like map and filter let you build large behaviors from small, pure functions.

Running Today

All examples can be run using Docker:

docker pull haskell:9.6
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining