compositions in haskell - way of working - haskell

I can't understand way of working composition in haskell,
for example:
((.) . (+)) 3 (*2) 9 = 9 * 2 + 3 = 21
This answer is generated by ghci.
For me, it should be:
((.) . (+)) 3 (*2) 9 = (3+9) * 2
Similarily, I can't understand why type of ((.) . (+)) is Num c => c -> (a -> c) -> a -> c
Can you try to explain it me ?

In this expression, (.) is applied to two arguments, (.) again and (+). Let's take it one step at a time. First, partially apply (.) to (.). We'll write the type signatures down explicitly, using different type variables to distinguish between the two:
(.) :: (b -> c) -> (a -> b) -> a -> c
(.) :: (t -> u) -> (s -> t) -> s -> u
Now look at the type of (.) (.) (the partial application). (This is probably the trickiest step, due to the size of the type expressions involved. The next step is the same, but with simpler types.) The first argument, of type (b -> c), is unified with (t -> u) -> (s -> t) -> s -> u, meaning
b ~ t -> u
c ~ (s -> t) -> s -> u
When we do the partial application, we drop the first argument to (.) (since we've given it a value) and look at the type after replacing the remaining bs and cs with their new types.
(.) :: (b -> c) -> (a -> b ) -> a -> c
(.) (.) :: (a -> (t -> u)) -> a -> ((s -> t) -> s -> u)
Because -> is right-associative, we can drop some of the excess parentheses to get
(.) (.) :: (a -> t -> u) -> a -> (s -> t) -> s -> u
Now, we apply that to (+) :: Num v => v -> v -> v (again, using a new type variable for clarity). The first argument of (.) (.) has type a -> t -> u, which we need to unify with v -> v -> v. In other words, we'll replace all occurrences of a, t, and u in (.) (.)'s type with v
(.) (.) :: (a -> t -> u) -> a -> (s -> t) -> s -> u
(.) (.) (+) :: v -> (s -> v) -> s -> v
And as you can see (after we add the Num v constraint back), this is equivalent to the type you see:
-- v ~ c, s ~ a
Num v => v -> (s -> v) -> s -> v
Num c => c -> (a -> c) -> a -> c
As for the actual expression being evaluated,
((.) . (+)) 3 (*2) 9
keep in mind the definition of (.)
f . g = \x -> f (g x)
and work from left to right. First, (.) . (+) is applied to 3.
((.) . (+)) 3 == (.) ((+) 3)
== (.) (3 +)
This partial application of . is applied to (* 2):
(.) (3 +) (* 2) == (3 +) . (* 2)
== \x -> (3 +) ((* 2) x)
Finally, this function is applied to 9:
(\x -> (3 +) ((* 2) x)) 9 == (3 +) ((* 2) 9)
== (3 +) (9 * 2)
== 3 + 9 * 2
== 3 + 18
== 21

Related

What is the type of the function g = (.).(.)?

The answer is: (a -> b) -> (c -> d -> a) -> c -> d -> b
But I don't know how to get there.
(.) has the type (b -> c) -> ((a -> b) -> (a -> c)). I intentionally added some parentheses for clarity. There are three instances of (.) in the expression (.) . (.) so having the type in three versions using different letters will be handy.
(.) :: (b -> c) -> ((a -> b) -> (a -> c)) – the first instance of (.): (.).(.)
(.) :: (e -> f) -> ((d -> e) -> (d -> f)) – the second instance of (.): (.). (.)
(.) :: (h -> i) -> ((g -> h) -> (g -> i)) – the third instance of (.): (.) .(.)
(.) . (.) is aequivalent to ((.) (.)) (.), it's applying (.) to (.) and then applying the result of the first application to (.).
Application of the first instance to the second instance
Match the type of the argument ((e -> f) -> ((d -> e) -> (d -> f))) to the input type of (.) (b -> c):
b = (e -> f)
c = ((d -> e) -> (d -> f))
Then substitue the type variables in the result type of (.) ((a -> b) -> (a -> c)) by the matches from the argument:
(.) (.) :: (a -> (e -> f)) -> (a -> ((d -> e) -> (d -> f)))
Application of the result of the first application to the third instance
Match the type of the argument ((h -> i) -> ((g -> h) -> (g -> i))) to the input type of (.) (.) (a -> (e -> f)):
a = (h -> i)
e = (g -> h)
f = (g -> i)
Then substitue the type variables in the result type of (.) (.) (a -> ((d -> e) -> (d -> f))) by the matches from the argument:
(.) (.) (.) :: (h -> i) -> ((d -> (g -> h)) -> (d -> (g -> i)))
That has the same type as what you have in the question, just with more parentheses and different letters. If I remove unnecessary parentheses, this is the result:
(.) (.) (.) :: (h -> i) -> (d -> g -> h) -> d -> g -> i
What does it do? It takes two arguments of types d and g, applies a function of type d -> g -> h to them, then applies a function of type h -> i to the result.
The type (.) :: (b->c) -> (a->b) -> (a->c) means
g :: a -> b
f :: d -> c
-------------------- d ~ b
f . g :: a -> c
and hence
(.) :: (b->c) -> ((a->b) -> (a->c))
(.) :: ( s -> r ) -> (t->s) -> (t->r)
-------------------------------------------------------------
(.) . (.) :: (b->c) -> (t->s) -> (t->r)
~ (b->c) -> (t->a->b) -> t->a->c
which makes sense, because
((.) . (.)) f g x y =
= (.) ((.) f) g x y
= ((f .) . g) x y = (f .) (g x) y
= (f . g x) y = f ( g x y )
because (.) f g x = (f .) g x = (f . g) x = f (g x) (and also = (. g) f x).
(f .) is known as operator section (with the (.) operator). It is a convenient shortcut for writing (.) f.

Understanding the signature of the function (.) (:)

could you explain how we go from :
Prelude Data.Monoid> :t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
Prelude Data.Monoid> :t (:)
(:) :: a -> [a] -> [a]
to that :
Prelude Data.Monoid> :t (.)(:)
(.)(:) :: (a1 -> a2) -> a1 -> [a2] -> [a2]
More in general, I'm sometimes afraid of (.) like I don't feel it intuitively, if you have some tricks to share to better feel it, it's welcome :-)
First, let's rename some things and put parenthesis:
(:) :: d -> ([d] -> [d])
Now, in the expresstion (.) (:) the (:) is the first argument of (.). The first argument of (.) should have type b -> c. Thus,
b -> c = d -> ([d] -> [d])
which means
b = d
c = [d] -> [d]
The result of (.) (:) has type (a -> b) -> a -> c. Putting our b and c in, we get
(a -> d) -> a -> ([d] -> [d])
This is exactly what ghci told you, except for the type variables renamed as a1 = a and a2 = d.
Well, let us do the type inference. We thus have two function:
(.) :: (b -> c) -> (a -> b) -> a -> c
(:) :: d -> [d] -> [d]
We here use d since the a in (.) is not per se the a in (:), so we avoid confusion by using two separate type variables.
The type signatures in a more canonic form are:
(.) :: (b -> c) -> ((a -> b) -> (a -> c))
(:) :: d -> ([d] -> [d])
So now since (:) is the argument of a function application with (.) as function, we know that the type of (:) is the type of the parameter of (.), so that means that d -> ([d] -> [d]) ~ (b -> c) (here the tilde ~ means that it is the same type). So therefore we know that:
b -> c
~ d -> ([d] -> [d])
---------------------
b ~ d, c ~ [d] -> [d]
So that means that the b in the type signature of (.) is the same type as d in the type signature of (:), and that c ~ [d] -> [d].
So the as a result, we get:
(.) (:) :: (a -> b) -> (a -> c))
= (.) (:) :: (a -> d) -> (a -> ([d] -> [d])))
= (.) (:) :: (a -> d) -> (a -> [d] -> [d])
= (.) (:) :: (a -> d) -> a -> [d] -> [d]
Writing (.) (:) as an operator section, ((:) .), emphasises a bit that it postcomposes (:) with some other function (i.e. it modifies a function by applying (:) to its result -- \g -> (g .) is fmap for functions). Symmetrically, (. (:)) precomposes (:) with some other function (\f -> (. f) is contramap for functions -- cf. the Op newtype).
(.) is nothing to fear. It is a most natural thing.
Imagine you define an abstract class of "connections":
class Connecting conn where
plug :: conn a b -> conn b c -> conn a c
noop :: conn a a
conn a b is a type of something which is connecting a point a to a point b. Given the possibility of connecting a to b, and b to c, it most naturally gives us the possibility of connecting a to c by just plugging the two somethings together. Also, any point which can be connected to another, and can get connected to, can obviously be considered connected to itself with absolutely no effort.
Functions are connecting. Just use the output of one as the input of another. If we have such two compatible functions, plugging them in that way gives us a combined connection.
Functions are Connecting:
instance Connecting (->) where
-- plug :: (->) a b -> (->) b c -> (->) a c
(plug f g) x = g (f x)
-- noop :: (->) a a
noop x = x -- what else? seriously. All we have is x.
Interesting thing about that plug :: (->) a b -> (->) b c -> (->) a c. That order of arguments is most fitting when we think about the types involved. But looking at its definition, we'd prefer it be defined as
gulp :: (->) b c -> (->) a b -> (->) a c
(gulp g f) x = g (f x)
Now the definition makes most sense, but the type feels a bit tortured.
Never mind that, we can have both:
(f :: a -> b) >>> (g :: b -> c) :: -- ...
a -> c
(g :: b -> c) <<< (f :: a -> b) :: -- ...
a ->
c
Coincidentally, <<< is also known as (.).
It is also clear that we have (.) = (<<<) = flip (>>>), so (g . f) x = g (f x) = (f >>> g) x.
Presented with g . f, it is often easier to deal with the equivalent expression f >>> g, types-wise. Just align the types, vertically,
(:) :: b -> ([b] -> [b])
f :: a -> b
f >>> (:) :: a -> ([b] -> [b])
so that
(>>> (:)) :: (a -> b) -- = ((:) <<<) = ((:) .) = (.) (:)
-> a -> ([b] -> [b])
because (>>> (:)) = (\f -> (f >>> (:))).
Which is (\f -> ((:) <<< f)) = (\f -> ((:) . f)) = ((:) .) = (.) (:).
edit: for instance, the type of (Endo . (:)) = ((:) >>> Endo) is easily arrived at, with:
Endo :: ( b -> b ) -> Endo b
(:) :: a -> ([a] -> [a]) -- b ~ [a]
(:) >>> Endo :: a -> Endo [a]
For more about Endo, try both :t Endo and :i Endo at GHCi prompt, and read up on Haskell's record syntax, if you need to.

Parenthesis around operator symbol in type signature

. :: (a -> b) -> (c -> a) -> c -> b
f . g = \ x -> f (g x)
Why is this definition illegal? Aesthetic choice, or formal necessity?
It's for consistency. Infix operators by themselves aren't expressions whose type or value you could define. The following is also not legal:
. = \f g x -> f (g x)
...though that is basically what your definition is desugared to. But for directly defining the value of an infix this way, we need to first disable the special syntactic status, and that's done by wrapping it in parentheses:
(.) :: (a -> b) -> (c -> a) -> c -> b
(.) = \f g x -> f (g x)
This is quite the same as defining
compose :: (a -> b) -> (c -> a) -> c -> b
compose = \f g x -> f (g x)
...which can then also be used as an infix, like
Prelude> map (toEnum `compose` round) $ [110 + sin x * 12 | x<-[0..30]] :: String
"nxypebkvzsgbhszvkbepyxndclwyqfb"
But you obviously wouldn't write the definition in infix form, like
`compose` :: (a -> b) -> (c -> a) -> c -> b
`compose` = \f g x -> f (g x)
...though you could do
f`compose`g = \x -> f (g x)

What is ((+) . (+)) in Haskell?

In ghci,
:t ((+).(+))
> ((+).(+)) :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a
but what is this thing? Can any one give me an example of the use of this please?
How can one composite 2 functions that take 2 parameters each?
for example, how does (map.map) :: (a -> b) -> [[a]] -> [[b]] work?
(^.^)
(-.-)
(+.+) (can't help making funny faces out of it. PS: I thought it means to tell the compiler how you feel today)
Num (a -> a) (or e.g. Eq (a -> a)) is basically an indicator for code that doesn't make any sense1, but the compiler nevertheless deduces a (nonsensical) type signature. Usually it turns up when you've forgotten to apply a function to some argument. In this case, obviously (+) needs a "plain number" argument to become a "simple function" to which you can post-compose another such function.
However, (a -> a) is sure enough a valid type of functions that you can also pass on, just not as numbers. For instance, map . (+) is a perfectly good combination:
Prelude> :t map . (+)
map . (+) :: Num b => b -> [b] -> [b]
Prelude> zipWith (map . (+)) [10,20,30] [[1,2],[3,4]]
[[11,12],[23,24]]
because map actually expects a function as its first argument. Similarly,
Prelude> zipWith (map . map) [(+10),(+20),(+30)] [[[1,2],[3,4]],[[5,6]]]
[[[11,12],[13,14]],[[25,26]]]
Here, the right map takes a simple function (like numerical increment) and returns the corresponding list-map function. That function is then fed to the left map resulting in a function that maps nested lists.
1Actually, you can force it to make sense by defining
instance (Num a) => Num (b -> a) where
fromInteger x = const $ fromInteger x
f + g = \x -> f x + g x
Personally, I'm not a fan of this. It confusing, for instance let a = 3 in 4 a produces 4 when most people would expect multiplication to 12.
That won't work. As ghci tells you, you should have an instance of Num (a -> a) in order to use that function, but a -> a obviously isn't a number.
This is because (+) assumes to get two numerical parameters, but with the composition you wrote you gave it a partially applied function instead, the a -> a mentioned in the computed type signature.
Usually, when composing functions which take more than one parameter, you partially apply them first in order to reduce them to functions which take just one parameter, e.g. (+1) . (*2) applied to 3 will result in (3 * 2) + 1 = 7
f . f can make sense for a binary function f; it entirely depends on the signature of f. The key is that partial application of the inner f to its first argument has to give something that is a valid input to the outer f.
For example, with map :: (a -> b) -> [a] -> [b], we can hand-unify map . map:
map :: (a -> b) -> [a] -> [b]
map :: (c -> d) -> [c] -> [d]
. :: (e -> f) -> (f -> g) -> (e -> g)
e === a -> b
f === [a] -> [b]
=== c -> d
c === [a]
d === [b]
g === [c] -> [d] === [[a]] -> [[b]]
map . map :: e -> g
:: (a -> b) -> [[a]] -> [[b]]
So, as expected, map . map takes a transformation a -> b and gives us a transformation from list-of-list-of-a to list-of-list-of-b. We can check this by hand-applying (map . map) f ll:
(map . map) f ll
= map (map f) ll
= map (\l -> map f l) ll
But if we try the same with (+) :: Num a => a -> a -> a, it all goes horribly wrong:
(+) :: Num a => a -> a -> a
(+) :: Num b => b -> b -> b
. :: (c -> d) -> (d -> e) -> (c -> e)
c === a
d === a -> a
=== b
e === b -> b === (a -> a) -> (a -> a)
(+) . (+) :: c -> e
:: (Num a, Num (a -> a)) => a -> (a -> a) -> (a -> a)
So, partial application of the inner + is giving a transformation a -> a, the outer + is then trying to add that transformation to another function which we are expected to supply. Since it doesn't make sense to add transformations, the overall (+) . (+) doesn't make sense either.
g . f means applying f first, then applying g to the result of f, in
other words, it can be rewritten as
\x -> g (f x)
Therefore,
((+) . (+))
can be rewritten as
\x -> (\y -> (x +) + y)
According to the type of (+), in the above lambda abstraction, x needs
having type Num a => a, y having type Num a => Num (a -> a), as inferred
by ghci
(Num a, Num (a -> a)) => a -> (a -> a) -> a -> a
So if we have made a -> a an instance of type class Num a, for example,
here is one way to achieve that
{-# LANGUAGE FlexibleInstances #-}
instance (Num a) => Num ((->) a a) where
a + b = \x -> a x + b x
a * b = \x -> a x * b x
a - b = \x -> a x - b x
negate a = \x -> negate $ a x
abs a = \x -> abs $ a x
signum a = \x -> signum $ a x
fromInteger n = \_x -> fromInteger n
we can use ((+) . (+)) like this
*Main> ((+) . (+)) 1 (+2) 3
9
Because ((+) . (+)) equals
\x -> \y -> (x +) + y
which means ((+) . (+)) 1 (+2) 3 equals
((1 + ) + (+ 2)) 3
according to the definition of (+) in the instance of (a -> a), ((1+) +
(+2)) equals
\x -> (1+x) + (x+2)
So ((1+) + (+2)) 3 equals (1+3) + (3+2), which is 9, as given by ghci.
map . map is similar, as indicated by its type, given by ghci:
(a -> b) -> [[a]] -> [[b]]
the first argument of that function should be a function of type a->b, the
second argument should be a nested list of type [[a]], and that composed
function map . map will apply the first argument to each element of each
list in its second argument, return a nested list of type [[b]]. For
example
*Main> (map . map) (+1) [[1,2], [3,4,5]]
[[2,3],[4,5,6]]

Composing function composition: How does (.).(.) work?

(.) takes two functions that take one value and return a value:
(.) :: (b -> c) -> (a -> b) -> a -> c
Since (.) takes two arguments, I feel like (.).(.) should be invalid, but it's perfectly fine:
(.).(.) :: (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c
What is going on here? I realize this question is badly worded...all functions really just take one argument thanks to currying. Maybe a better way to say it is that the types don't match up.
Let's first play typechecker for the mechanical proof. I'll describe an intuitive way of thinking about it afterward.
I want to apply (.) to (.) and then I'll apply (.) to the result. The first application helps us to define some equivalences of variables.
((.) :: (b -> c) -> (a -> b) -> a -> c)
((.) :: (b' -> c') -> (a' -> b') -> a' -> c')
((.) :: (b'' -> c'') -> (a'' -> b'') -> a'' -> c'')
let b = (b' -> c')
c = (a' -> b') -> a' -> c'
((.) (.) :: (a -> b) -> a -> c)
((.) :: (b'' -> c'') -> (a'' -> b'') -> a'' -> c'')
Then we begin the second, but get stuck quickly...
let a = (b'' -> c'')
This is key: we want to let b = (a'' -> b'') -> a'' -> c'', but we already defined b, so instead we must try to unify --- to match up our two definitions as best we can. Fortunately, they do match
UNIFY b = (b' -> c') =:= (a'' -> b'') -> a'' -> c''
which implies
b' = a'' -> b''
c' = a'' -> c''
and with those definitions/unifications we can continue the application
((.) (.) (.) :: (b'' -> c'') -> (a' -> b') -> (a' -> c'))
then expand
((.) (.) (.) :: (b'' -> c'') -> (a' -> a'' -> b'') -> (a' -> a'' -> c''))
and clean it up
substitute b'' -> b
c'' -> c
a' -> a
a'' -> a1
(.).(.) :: (b -> c) -> (a -> a1 -> b) -> (a -> a1 -> c)
which, to be honest, is a bit of a counterintuitive result.
Here's the intuition. First take a look at fmap
fmap :: (a -> b) -> (f a -> f b)
it "lifts" a function up into a Functor. We can apply it repeatedly
fmap.fmap.fmap :: (Functor f, Functor g, Functor h)
=> (a -> b) -> (f (g (h a)) -> f (g (h b)))
allowing us to lift a function into deeper and deeper layers of Functors.
It turns out that the data type (r ->) is a Functor.
instance Functor ((->) r) where
fmap = (.)
which should look pretty familiar. This means that fmap.fmap translates to (.).(.). Thus, (.).(.) is just letting us transform the parametric type of deeper and deeper layers of the (r ->) Functor. The (r ->) Functor is actually the Reader Monad, so layered Readers is like having multiple independent kinds of global, immutable state.
Or like having multiple input arguments which aren't being affected by the fmaping. Sort of like composing a new continuation function on "just the result" of a (>1) arity function.
It's finally worth noting that if you think this stuff is interesting, it forms the core intuition behind deriving the Lenses in Control.Lens.
Let’s ignore types for a moment and just use lambda calculus.
Desugar infix notation:
(.) (.) (.)
Eta-expand:
(\ a b -> (.) a b) (\ c d -> (.) c d) (\ e f -> (.) e f)
Inline the definition of (.):
(\ a b x -> a (b x)) (\ c d y -> c (d y)) (\ e f z -> e (f z))
Substitute a:
(\ b x -> (\ c d y -> c (d y)) (b x)) (\ e f z -> e (f z))
Substitute b:
(\ x -> (\ c d y -> c (d y)) ((\ e f z -> e (f z)) x))
Substitute e:
(\ x -> (\ c d y -> c (d y)) (\ f z -> x (f z)))
Substitute c:
(\ x -> (\ d y -> (\ f z -> x (f z)) (d y)))
Substitute f:
(\ x -> (\ d y -> (\ z -> x (d y z))))
Resugar lambda notation:
\ x d y z -> x (d y z)
And if you ask GHCi, you’ll find that this has the expected type. Why? Because the function arrow is right-associative to support currying: the type (b -> c) -> (a -> b) -> a -> c really means (b -> c) -> ((a -> b) -> (a -> c)). At the same time, the type variable b can stand for any type, including a function type. See the connection?
Here is a simpler example of the same phenomenon:
id :: a -> a
id x = x
The type of id says that id should take one argument. And indeed, we can call it with one argument:
> id "hello"
"hello"
But it turns out what we can also call it with two arguments:
> id not True
False
Or even:
> id id "hello"
"hello"
What is going on? The key to understanding id not True is to first look at id not. Clearly, that's allowed, because it applies id to one argument. The type of not is Bool -> Bool, so we know that the a from id's type should be Bool -> Bool, so we know that this occurrence of id has type:
id :: (Bool -> Bool) -> (Bool -> Bool)
Or, with less parentheses:
id :: (Bool -> Bool) -> Bool -> Bool
So this occurrence of id actually takes two arguments.
The same reasoning also works for id id "hello" and (.) . (.).
This is one of those neat cases where I think it's simpler to grasp the more general case first, and then think about the specific case. So let's think about functors. We know that functors provide a way to map functions over a structure --
class Functor f where
fmap :: (a -> b) -> f a -> f b
But what if we have two layers of the functor? For example, a list of lists? In that case we can use two layers of fmap
>>> let xs = [[1,2,3], [4,5,6]]
>>> fmap (fmap (+10)) xs
[[11,12,13],[14,15,16]]
But the pattern f (g x) is exactly the same as (f . g) x so we could write
>>> (fmap . fmap) (+10) xs
[[11,12,13],[14,15,16]]
What is the type of fmap . fmap?
>>> :t fmap.fmap
:: (Functor g, Functor f) => (a -> b) -> f (g a) -> f (g b)
We see that it maps over two layers of functor, as we wanted. But now remember that (->) r is a functor (the type of functions from r, which you might prefer to read as (r ->)) and its functor instance is
instance Functor ((->) r) where
fmap f g = f . g
For a function, fmap is just function composition! When we compose two fmaps we map over two levels of the function functor. We initially have something of type (->) s ((->) r a), which is equivalent to s -> r -> a, and we end up with something of type s -> r -> b, so the type of (.).(.) must be
(.).(.) :: (a -> b) -> (s -> r -> a) -> (s -> r -> b)
which takes its first function, and uses it to transform the output of the second (two-argument) function. So for example, the function ((.).(.)) show (+) is a function of two arguments, that first adds its arguments together and then transforms the result to a String using show:
>>> ((.).(.)) show (+) 11 22
"33"
There is then a natural generalization to thinking about longer chains of fmap, for example
fmap.fmap.fmap ::
(Functor f, Functor g, Functor h) => (a -> b) -> f (g (h a)) -> f (g (h b))
which maps over three layers of functor, which is equivalent to composing with a function of three arguments:
(.).(.).(.) :: (a -> b) -> (r -> s -> t -> a) -> (r -> s -> t -> b)
for example
>>> import Data.Map
>>> ((.).(.).(.)) show insert 1 True empty
"fromList [(1,True)]"
which inserts the value True into an empty map with key 1, and then converts the output to a string with show.
These functions can be generally useful, so you sometimes see them defined as
(.:) :: (a -> b) -> (r -> s -> a) -> (r -> s -> b)
(.:) = (.).(.)
so that you can write
>>> let f = show .: (+)
>>> f 10 20
"30"
Of course, a simpler, pointful definition of (.:) can be given
(.:) :: (a -> b) -> (r -> s -> a) -> (r -> s -> b)
(f .: g) x y = f (g x y)
which may help to demystify (.).(.) somewhat.
You're right, (.) only takes two arguments. You just seem to be confused with the syntax of haskell. In the expression (.).(.), it's in fact the dot in the middle that takes the other two dots as argument, just like in the expression 100 + 200, which can be written as (+) 100 200.
(.).(.) === (number the dots)
(1.)2.(3.) === (rewrite using just syntax rules)
(2.)(1.)(3.) === (unnumber and put spaces)
(.) (.) (.) ===
And it should be even more clear from (.) (.) (.) that the first (.) is taking the second (.) and third (.) as it's arguments.
Yes this is due to currying. (.) as all functions in Haskell only takes one argument. What you are composing is the first partial call to each respective composed (.) which takes its first argument (the first function of the composition).
(Read my answer on function composition, $ operator and point-free style first.)
Imagine you have a simple function: it adds up 2 numbers and then negates the result. We'll call it foo:
foo a b = negate (a + b)
Now let's make it point-free step by step and see what we end up with:
foo a b = negate $ a + b
foo a b = negate $ (+) a b
foo a b = negate $ (+) a $ b
foo a b = negate . (+) a $ b
foo a = negate . (+) a -- f x = g x is equivalent to f = g
foo a = (.) negate ((+) a) -- any infix operator is just a function
foo a = (negate.) ((+) a) -- (2+) is the same as ((+) 2)
foo a = (negate.) $ (+) a
foo a = (negate.) . (+) $ a
foo = (negate.) . (+)
foo = ((.) negate) . (+)
foo = (.) ((.) negate) (+) -- move dot in the middle in prefix position
foo = ((.) ((.) negate)) (+) -- add extra parentheses
Now let's analyze expression (.) ((.) negate) more closely. It's a partial application of (.) function, whose first argument is ((.) negate). Can we transform it even further? Yes we can:
(.) ((.) negate)
(.) . (.) $ negate -- because f (f x) is the same as (f . f) x
(.)(.)(.) $ negate
((.)(.)(.)) negate
(.).(.) is equivalent to (.)(.)(.), because in the 1st expression, the dot in the middle can be moved in prefix position and surrounded with parentheses, which gives rise to the 2nd expression.
Now we can rewrite our foo function:
foo = ((.).(.)) negate (+)
foo = ((.)(.)(.)) negate (+) -- same as previous one
foo = negate .: (+)
where (.:) = (.).(.)
Now you know that (.).(.) is equivalent to (\f g x y -> f (g x y)):
(\f g x y -> f (g x y)) negate (+) 2 3 -- returns -5
((.).(.)) negate (+) 2 3 -- returns -5

Resources