Haskell composition operator fun: (.).(.)

I've been learning Haskell over the summer, and have definitely had my fair share of head-scratching moments!

Having seen this reddit thread, I attempted to figure out the type of the expression (.).(.) which is the infix representation of (.)(.)(.) (the function composition operator, applied to itself twice).

This was not an easy task for a novice Haskeller such as myself, so hopefully others may benefit from my notes that follow.

The type of the operator (.) is:

(.) :: (b -> c) -> (a -> b) -> a -> c

The first step of figuring out the type of (.)(.)(.) is to calculate the type of (.)(.)

Since we are applying the (.) section (a section is a partially applied operator) to itself, we shall rename the parameter types to avoid confusion. The first argument, (.) (we shall call it (.)1) therefore has the following type:

(.)1 :: (b1 -> c1) -> (a1 -> b1) -> a1 -> c1

When applying (.) to the single argument (.)1 we are 'substituting'

(b -> c)

for

(b1 -> c1) -> (a1 -> b1) -> a1 -> c1

taking a three-parameter function and passing it to something that expects a one-parameter function. Therefore, the result is a two-parameter function. This is as if the original function took a a two-parameter function as a parameter.

Matching the types gives us the following 'mappings':

b :: (b1 -> c1)

and

c :: (a1 -> b1) -> a1 -> c1

In (.)'s signature, the result of providing a single argument is of type: (a -> b) -> a -> c

but using the type 'mappings', we obtain a result that looks like:

(.)(.) :: (a -> (b1 -> c1)) -> a -> (a1 -> b1) -> a1 -> c1

Notice how we've replaced the original single parameter with two new parameters.

We can then repeat for the second argument of (.), which we shall name (.)2 and has type:

(b2 -> c2) -> (a2 -> b2) -> a2 -> c2

N.B. (.)(.) takes 4 parameters, the first being a function of type a -> (b1 -> c1).

When we apply (.)(.) to (.) we are providing its first parameter, which gives us type mappings of:

a :: (b2 -> c2)

and

(b1 -> c1) :: (a2 -> b2) -> a2 -> c2

therefore:

b1 :: (a2 -> b2)

and

c1 :: a2 -> c2

The result of applying (.)(.) to a single argument is a -> (a1 -> b1) -> a1 -> c1 But using our mappings we see that the type is:

(.)(.)(.) :: (b2 -> c2) -> (a1 -> a2 -> b2) -> a1 -> a2 -> c2

We can rename some types to simplify to the result given by GHCi:

(.)(.)(.) :: (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c

Which is a function that takes a single-parameter function, a two-parameter function, and two values. The result is obtained by applying the two-parameter function to the two values and passing the result to the single-parameter function.

...and with that we've solved the puzzle!