4 min read
On this page

Higher-Order Functions

A higher-order function is one that takes a function as an argument, returns a function as a result, or both. In Haskell this is unremarkable. The standard library is mostly higher-order functions: map, filter, foldr, flip, on, (.), ($). Once you stop noticing the term, you've crossed a threshold.

The reason higher-order functions matter so much in Haskell is that they let you separate "what to do" from "how to walk the structure." map knows how to walk a list. You supply the per-element work. foldr knows how to collapse a list. You supply the combining step. The same shape generalizes to trees, maps, futures, and effects.

Functions as Parameters

The most familiar example is map:

map :: (a -> b) -> [a] -> [b]
map _ []     = []
map f (x:xs) = f x : map f xs

The first argument is (a -> b), a function. You'd use it like this:

import Data.Char (toUpper)

shouted :: [String] -> [String]
shouted = map (map toUpper)

-- shouted ["hi", "there"] == ["HI", "THERE"]

The outer map walks the list of strings; the inner map walks each string (a list of Char). Same function, two structures.

Custom higher-order functions are just as easy. Suppose you're processing log entries and want a generic "retry on failure" wrapper:

retry :: Int -> IO (Either e a) -> IO (Either e a)
retry 0 action = action
retry n action = do
  result <- action
  case result of
    Right x  -> pure (Right x)
    Left _   -> retry (n - 1) action

The argument IO (Either e a) is a value, but it's an action that hasn't run yet. retry 3 fetchUser builds a new action that runs fetchUser up to four times before giving up.

Returning Functions

Returning functions is the flip side. Every curried function technically returns a function, but it's worth being explicit when the intent is "build a specialized worker":

makeAdder :: Int -> (Int -> Int)
makeAdder n = \x -> x + n

addTen :: Int -> Int
addTen = makeAdder 10

-- More realistic: build a logger configured with a prefix
makeLogger :: String -> (String -> IO ())
makeLogger prefix = \msg -> putStrLn (prefix ++ ": " ++ msg)

main :: IO ()
main = do
  let warn = makeLogger "WARN"
  let info = makeLogger "INFO"
  warn "disk almost full"
  info "request handled"

This pattern is everywhere in real code. Configuration, database handles, HTTP clients, and authentication tokens all get baked into specialized functions at startup, and the rest of the program calls those.

The $ Operator

$ is function application with very low precedence:

($) :: (a -> b) -> a -> b
f $ x = f x

It does nothing semantically. f $ x and f x produce the same value. The point is that $ has the lowest precedence of any operator, while normal function application has the highest. This lets you avoid parentheses:

-- Without $
print (sqrt (fromIntegral (length "hello")))

-- With $
print $ sqrt $ fromIntegral $ length "hello"

Read it right to left: take the length, convert to a Double, square root it, print it. Each $ says "take everything on the right as a single argument."

$ is also the conventional way to apply a complex argument to do-notation:

-- Pass a multi-line lambda to forM_
forM_ users $ \user -> do
  putStrLn (userName user)
  saveToDb user
  notify user

Without $, you'd need parentheses around the entire lambda block.

The . Operator

. is function composition:

(.) :: (b -> c) -> (a -> b) -> (a -> c)
(f . g) x = f (g x)

f . g is the function "do g, then do f." Note the order: the function on the right runs first. This matches mathematical notation where (f ∘ g)(x) = f(g(x)).

-- Length of the uppercased version of a string
shoutLength :: String -> Int
shoutLength = length . map toUpper

-- Equivalent
shoutLength s = length (map toUpper s)

$ vs .

This trips up almost everyone learning Haskell. The two operators look similar but do different things:

  • $ applies a function to a value. Result: a value.
  • . glues two functions together. Result: a function.

Compare:

-- $ takes a function and a value
result1 :: Int
result1 = length $ "hello world"     -- 11

-- . takes two functions and produces a function
combined :: String -> Int
combined = length . reverse           -- still a function

-- Use . when you're building a pipeline you'll apply later
shout :: String -> String
shout = map toUpper . filter (/= ' ')

-- Use $ when you have a value at the end
main :: IO ()
main = putStrLn $ shout "hello world"

A common idiom mixes both:

-- Compose a pipeline (with .), then apply it to a value (with $)
main = print $ sum . map (^ 2) . filter even $ [1..100]

The pipeline sum . map (^ 2) . filter even is built with ., then $ applies it to [1..100], then another $ applies print to the result.

Useful Higher-Order Helpers

A handful of standard combinators show up constantly. Knowing them by name makes Haskell code easier to read.

flip

flip :: (a -> b -> c) -> b -> a -> c
flip f x y = f y x

Swaps the first two arguments of a function:

-- divide is x / y, but we want a "divide INTO" function
divInto :: Double -> Double -> Double
divInto = flip (/)

-- divInto 2 10 == 5.0  (10 / 2)

flip is most useful when the wrong argument order is fighting partial application:

-- subtract is already flip (-)
-- subtract 3 10 == 7  (because it's 10 - 3)

on

on lives in Data.Function and is one of the most underused tools in the standard library:

on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
(f `on` g) x y = f (g x) (g y)

It's the "compare by some projection" combinator:

import Data.Function (on)
import Data.List (sortBy)

data Person = Person { name :: String, age :: Int }

-- Sort people by age
byAge :: [Person] -> [Person]
byAge = sortBy (compare `on` age)

-- Group by first letter of name
import Data.List (groupBy)
import Data.Char (toLower)

byInitial :: [Person] -> [[Person]]
byInitial = groupBy ((==) `on` (toLower . head . name))

Without on, you'd write sortBy (\a b -> compare (age a) (age b)), which is fine but noisier.

curry and uncurry

These convert between curried and tuple-taking forms:

curry   :: ((a, b) -> c) -> a -> b -> c
uncurry :: (a -> b -> c) -> (a, b) -> c

You almost never need curry. uncurry shows up when you have a list of pairs and want to apply a function to each:

-- pair-wise multiplication
products :: [(Int, Int)] -> [Int]
products = map (uncurry (*))

-- products [(2, 3), (4, 5)] == [6, 20]

zip returns pairs; uncurry lets you feed those pairs to a two-argument function without unpacking by hand.

Real Example: Composing a Validation Pipeline

Here's a sketch from a real form-validation scenario:

import Data.Char (isDigit, isAlpha)

-- Each check returns Either error value
type Check a = a -> Either String a

notEmpty :: Check String
notEmpty "" = Left "must not be empty"
notEmpty s  = Right s

minLength :: Int -> Check String
minLength n s
  | length s >= n = Right s
  | otherwise     = Left ("must be at least " ++ show n ++ " characters")

allAlpha :: Check String
allAlpha s
  | all isAlpha s = Right s
  | otherwise     = Left "must be letters only"

-- Combine checks with monadic bind (>>=)
validateName :: Check String
validateName s = notEmpty s >>= minLength 2 >>= allAlpha

Each check is a higher-order-friendly function. minLength even returns one. The composition uses >>=, which is itself a higher-order function.

Common Pitfalls

Confusing $ and .. Type errors involving these operators are usually shape mismatches. If you see "couldn't match expected type a -> b with b," you probably wrote f . x where x is a value, not a function. Use f $ x.

Overusing flip. If you're flipping arguments often, the function probably has the wrong argument order. Fix the definition instead.

Forgetting that . reads right to left. Newcomers from Unix piping (|) want the order reversed. Control.Category provides >>> for left-to-right composition; some teams adopt it for clarity (more on that next).

Hiding logic in deep compositions. f = a . b . c . d . e . g . h is opaque. If a reviewer can't tell what each step contributes, name the intermediate steps.

Key Takeaways

Higher-order functions are the spine of Haskell. The standard library gives you map, filter, foldr, flip, on, curry, and uncurry, and they cover most needs. $ is low-precedence application that lets you skip parentheses; . glues functions into pipelines. Use $ when you have a value to feed in; use . when you're building a transformation to apply later. Knowing on and flip by name will save you a thousand lambdas.