The Application and Composition Operators

The Composition Operator

Composing functions is a common and useful way to create new functions in Haskell. Haskell composition is based on the idea of function composition in mathematics. In mathematics, if you have two functions \(f(x)\) and \(g(x)\), you compute their composition as \(f(g(x))\). The expression \(f(g(x))\) first calls \(g\) and then calls \(f\). We can combine the functions to create a brand new function, \(h\), that returns the same thing as the two individual function calls. In mathematics, we would write \(h = f \circ g\), i.e. \(h\) is the composition of \(f\) and \(g\).

Haskell mirrors this notation by using . to compose functions. For example:

> f :: Int -> Int
> f n = n + 1
> g :: Int -> Int
> g n = 2*n - 1
> h = f . g  -- h is the composition of f and g

For example:

> f(g(3))
> h(3)

What is the type of .? Think about this before looking at the answer!

According to Haskell:

> :type (.)        -- . must be written (.) to avoid a parser error
(.) :: (b -> c) -> (a -> b) -> a -> c

That’s quite a mouthful! To understand it, let’s first observe a few basic things:

  • (.) is a function that takes three inputs. We know this because the type signature has the form W -> X -> Y -> Z, where W, X, and Y are the three inputs to the function, and Z is its output.
  • The first input to (.) has the type b -> c. That means the first input is a function that takes a value of type b as input, and returns a value of type c as output.
  • Similarly, we can see that the second input to (.) is also a function since it has the type a -> b.

To understand it further, lets name the functions involved in the composition:

h = f . g

Notice that:

  • The signature of (.) tells us that f has type b -> c, g has type a -> b, and h has the type a -> c.
  • From the definition of composition, h x is the same as f (g x). In the expression f (g x), g x is calculated first, and since g has the type a -> b, x must be a value of type a.
  • Since (g x) returns a value of type b, we can use that value as the input to f, because the type of f is b -> c.
  • Finally, the output of f is a value of type c, so that means the output of h is also a value of type c.

Take some time to work through this and understand why all the types are what they are. A trick to help remember the type of (.) is to start by giving g (the second input) the type a -> b.

One of the uses of . in Haskell is to cut down on brackets. For example, here are two equivalent ways to do the same calculation:

> sqrt (abs (negate 2))   -- writing -2 gives a parser error, so we use
1.4142135623730951        -- negate instead

> (sqrt . abs . negate) 2

Notice that sqrt . abs . negate lists the functions that are applied in reverse order, i.e. first negate is applied, and then abs, and then sqrt.

. also lets us define some functions more compactly, e.g.:

> root1 :: Float -> Float
> root1 x = sqrt (abs (negate x))
> root2 :: Float -> Float
> root2 = sqrt . abs . negate   -- point free style
> mylast = head . reverse       -- point free style

root2 is written in what is sometimes called a point free style. All that means is the input parameter, x in this case, is not written explicitly.

The difference between function composition and regular function application can sometimes seem confusing. So it might be helpful to remember this example:

-- Composition
f (g (h x))      -- these two expressions evaluate
(f . g . h) x    -- to the same thing

-- Application
f g h x          -- these two expressions evaluate
((f g) h) x      -- to the same thing
                 -- (but probably not the same as the two
                 -- previous composition expressions!)

A function application like f g h x is a call to the function f, and g, h, and x are the parameters passed to it. The parameters are passed in the order they are given, i.e. first g is passed to f, then h, and finally x. Thus, function application is left-associative.

$: The Application Operator

The $ operator may seem unusual at first since its definition is so simple:

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

$ is called the function application operator, and its implementation apparently says that is the same as f x. So what is the point of $?

What makes $ useful is that it has the lowest possible precedence for any infix operator. Recall that for infix operators it is necessary to assign a precedence so we know in what order to evaluate them. For example, to evaluate \(2 + 3\cdot 4\), we need to know that \(\cdot\) has higher precedence than addition, and so must be evaluated first.

The :info command will show you the precedence of an infix operator, e.g.:

> :info ($)                 -- $ must be written ($) to avoid a parser error
($) :: (a -> b) -> a -> b   -- Defined in `GHC.Base'
infixr 0 $

So if you see a $ in a Haskell expression, it is evaluated last because it has such low priority. It essentially changes function application to be right-associative instead of left-associative.

The main use of $ is to simplify some expressions by removing brackets. For example:

> sum (map (+1) [1,2,3])

Brackets are necessary around the map expression. If you left them out, sum would try to apply itself to map, which results in an error:

> sum map (+1) [1,2,3]

Couldn't match expected type `[(a1 -> a1) -> [t1] -> t0]'
            with actual type `(a0 -> b0) -> [a0] -> [b0]'
In the first argument of `sum', namely `map'
In the expression: sum map (+ 1) [1, 2, 3]
In an equation for `it': it = sum map (+ 1) [1, 2, 3]

But by using $, we can make the function application right-associative, and thus get rid of a pair of brackets:

> sum $ map (+1) [1,2,3]

The thing to remember with $ is that it has the lowest possible precedence of all infix operators, and so when you see it in an expression it is evaluated last. So in this example map (+1) [1,2,3] is evaluated first, and then sum is applied to its result — which is exactly how we want to do things.