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))
6
> h(3)
6
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 formW -> X -> Y -> Z
, whereW
,X
, andY
are the three inputs to the function, andZ
is its output.- The first input to
(.)
has the typeb -> c
. That means the first input is a function that takes a value of typeb
as input, and returns a value of typec
as output. - Similarly, we can see that the second input to
(.)
is also a function since it has the typea -> b
.
To understand it further, lets name the functions involved in the composition:
h = f . g
Notice that:
- The signature of
(.)
tells us thatf
has typeb -> c
,g
has typea -> b
, andh
has the typea -> c
. - From the definition of composition,
h x
is the same asf (g x)
. In the expressionf (g x)
,g x
is calculated first, and sinceg
has the typea -> b
, x must be a value of typea
. - Since
(g x)
returns a value of typeb
, we can use that value as the input tof
, because the type off
isb -> c
. - Finally, the output of
f
is a value of typec
, so that means the output ofh
is also a value of typec
.
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
1.4142135623730951
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])
9
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]
9
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.