Point free composition of multivariate functions - haskell

Say, we want to introduce the notion of sum of functions of different arguments (let's call it <+>), which behaves like the that: (f1 <+> f2)(x1, x2) == f1(x1) + f2(x2).
While this can be easily written out manually, it makes sense to use point-free style with the help of the notion of cartesian product of functions. The latter is defined below and seems alright and quite general to me:
x :: (x1 -> y1) -> (x2 -> y2) -> (x1 -> x2 -> (y1, y2))
x f1 f2 = \x1 x2 -> (f1(x1), f2(x2))
Then we can write:
(<+>):: Num a => (a -> a) -> (a -> a) -> (a -> a -> a)
(<+>) = (uncurry (+)) . x
And the code above seems fine to me too, but GHC thinks otherwise:
* Couldn't match type: (x20 -> y20) -> a -> x20 -> (a, y20)
with: ((a -> a) -> a -> a -> a, (a -> a) -> a -> a -> a)
Expected: (a -> a)
-> ((a -> a) -> a -> a -> a, (a -> a) -> a -> a -> a)
Actual: (a -> a) -> (x20 -> y20) -> a -> x20 -> (a, y20)
* Probable cause: `x' is applied to too few arguments
In the second argument of `(.)', namely `x'
In the expression: (uncurry (+)) . x
In an equation for `<+>': (<+>) = (uncurry (+)) . x
* Relevant bindings include
(<+>) :: (a -> a) -> (a -> a) -> a -> a -> a
It feels like the compiler cannot infer the second function's type, but why? And what am I supposed to do, is this even possible to do?

If you supply two arguments, you will see what has gone wrong.
(<+>) = uncurry (+) . x
(<+>) a = (uncurry (+) . x) a
= uncurry (+) (x a)
(<+>) a b = uncurry (+) (x a) b
Whoops! That b gets passed to uncurry as a third argument, rather than x as a second argument as you probably intended. The third and fourth arguments are also supposed to go to x rather than uncurry, as in:
(<+>) a b c d = uncurry (+) (x a b c d)
Here's the correct way to point-free-ify a four-argument composition.
\a b c d -> f (g a b c d)
= \a b c d -> (f . g a b c) d
= \a b c -> f . g a b c
= \a b c -> ((.) f . g a b) c
= \a b -> (.) f . g a b
= \a b -> ((.) ((.) f) . g a) b
= \a -> (.) ((.) f) . g a
= \a -> ((.) ((.) ((.) f)) . g) a
= (.) ((.) ((.) f)) . g
Most people then write this with section syntax as (((f .) .) .) . g. Applying this new fact to your case:
\a b c d -> uncurry (+) (x a b c d)
= (((uncurry (+) .) .) .) . x

The . operator is only for composing functions with a single argument, but the function x has four arguments, so you have to use . four times:
(<+>) = (((uncurry (+) .) .) .) . x
Do keep in mind that this is not considered good style in actual code.

Define
compose2 :: (b -> c -> t) -> (a -> b) -> (d -> c) -> a -> d -> t
compose2 p f g x y = p (f x) (g y)
Now, compose2 (+) is your <+>:
> :t compose2 (+)
compose2 (+) :: Num t => (a -> t) -> (d -> t) -> a -> d -> t
As you can see its type is a bit more general than you thought.
compose2 already exists.

Related

Multiple folds in one pass using generic tuple function

How can I write a function which takes a tuple of functions of type ai -> b -> ai and returns a function which takes a tuple of elements of type ai, one element of type b, and combines each of the elements into a new tuple of ai:
That is the signature should be like
f :: (a1 -> b -> a1, a2 -> b -> a2, ... , an -> b -> an) ->
(a1, a2, ... , an) ->
b ->
(a1, a2, ... , an)
Such that:
f (min, max, (+), (*)) (1,2,3,4) 5 = (1, 5, 8, 20)
The point of this is so I can write:
foldlMult' t = foldl' (f t)
And then do something like:
foldlMult' (min, max, (+), (*)) (head x, head x, 0, 0) x
to do multiple folds in one pass. GHC extensions are okay.
If I understand your examples right, the types are ai -> b -> ai, not ai -> b -> a as you first wrote. Let's rewrite the types to a -> ri -> ri, just because it helps me think.
First thing to observe is this correspondence:
(a -> r1 -> r1, ..., a -> rn -> rn) ~ a -> (r1 -> r1, ..., rn -> rn)
This allows you to write these two functions, which are inverses:
pullArg :: (a -> r1 -> r1, a -> r2 -> r2) -> a -> (r1 -> r1, r2 -> r2)
pullArg (f, g) = \a -> (f a, g a)
pushArg :: (a -> (r1 -> r1, r2 -> r2)) -> (a -> r1 -> r1, a -> r2 -> r2)
pushArg f = (\a -> fst (f a), \a -> snd (f a))
Second observation: types of the form ri -> ri are sometimes called endomorphisms, and each of these types has a monoid with composition as the associative operation and the identity function as the identity. The Data.Monoid package has this wrapper for that:
newtype Endo a = Endo { appEndo :: a -> a }
instance Monoid (Endo a) where
mempty = id
mappend = (.)
This allows you to rewrite the earlier pullArg to this:
pullArg :: (a -> r1 -> r1, a -> r2 -> r2) -> a -> (Endo r1, Endo r2)
pullArg (f, g) = \a -> (Endo $ f a, Endo $ g a)
Third observation: the product of two monoids is also a monoid, as per this instance also from Data.Monoid:
instance (Monoid a, Monoid b) => Monoid (a, b) where
mempty = (mempty, mempty)
(a, b) `mappend` (c, d) = (a `mappend` c, b `mappend d)
Likewise for tuples of any number of arguments.
Fourth observation: What are folds made of? Answer: folds are made of monoids!
import Data.Monoid
fold :: Monoid m => (a -> m) -> [a] -> m
fold f = mconcat . map f
This fold is just a specialization of foldMap from Data.Foldable, so in reality we don't need to define it, we can just import its more general version:
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
If you fold with Endo, that's the same as folding from the right. To fold from the left, you want to fold with the Dual (Endo r) monoid:
myfoldl :: (a -> Dual (Endo r)) -> r -> -> [a] -> r
myfoldl f z xs = appEndo (getDual (fold f xs)) z
-- From `Data.Monoid`. This just flips the order of `mappend`.
newtype Dual m = Dual { getDual :: m }
instance Monoid m => Monoid (Dual m) where
mempty = Dual mempty
Dual a `mappend` Dual b = Dual $ b `mappend` a
Remember our pullArg function? Let's revise it a bit more, since you're folding from the left:
pullArg :: (a -> r1 -> r1, a -> r2 -> r2) -> a -> Dual (Endo r1, Endo r2)
pullArg (f, g) = \a -> Dual (Endo $ f a, Endo $ g a)
And this, I claim, is the 2-tuple version of your f, or at least isomorphic to it. You can refactor your fold functions into the form a -> Endo ri, and then do:
let (f'1, ..., f'n) = foldMap (pullArgn f1 ... fn) xs
in (f'1 z1, ..., f'n zn)
Also worth looking at: Composable Streaming Folds, which is a further elaboration of these ideas.
For a direct approach, you can just define the equivalents of Control.Arrow's (***) and (&&&) explicitly, for each N (e.g. N == 4):
prod4 (f1,f2,f3,f4) (x1,x2,x3,x4) = (f1 x1,f2 x2,f3 x3,f4 x4) -- cf (***)
call4 (f1,f2,f3,f4) x = (f1 x, f2 x, f3 x, f4 x ) -- cf (&&&)
uncurry4 f (x1,x2,x3,x4) = f x1 x2 x3 x4
Then,
foldr4 :: (b -> a1 -> a1, b -> a2 -> a2,
b -> a3 -> a3, b -> a4 -> a4)
-> (a1, a2, a3, a4) -> [b]
-> (a1, a2, a3, a4) -- (f .: g) x y = f (g x y)
foldr4 t z xs = foldr (prod4 . call4 t) z xs -- foldr . (prod4 .: call4)
-- f x1 (f x2 (... (f xn z) ...)) -- foldr . (($) .: ($))
So the tuple's functions in foldr4's are flipped versions of what you wanted. Testing:
Prelude> g xs = foldr4 (min, max, (+), (*)) (head xs, head xs, 0, 1) xs
Prelude> g [1..5]
(1,5,15,120)
foldl4' is a tweak away. Since
foldr f z xs == foldl (\k x r-> k (f x r)) id xs z
foldl f z xs == foldr (\x k a-> k (f a x)) id xs z
we have
foldl4, foldl4' :: (t -> a -> t, t1 -> a -> t1,
t2 -> a -> t2, t3 -> a -> t3)
-> (t, t1, t2, t3) -> [a]
-> (t, t1, t2, t3)
foldl4 t z xs = foldr (\x k a-> k (call4 (prod4 t a) x))
(prod4 (id,id,id,id)) xs z
foldl4' t z xs = foldr (\x k a-> k (call4 (prod4' t a) x))
(prod4 (id,id,id,id)) xs z
prod4' (f1,f2,f3,f4) (x1,x2,x3,x4) = (f1 $! x1,f2 $! x2,f3 $! x3,f4 $! x4)
We've even got the types as you wanted, for the tuple's functions.
A stricter version of prod4 had to be used to force the arguments early in foldl4'.

($) is to (.) as `fmap` is to?

I have a function funcM :: a -> b -> c -> IO (x, y)
I want to write a function funcM_ :: a-> b-> c-> IO x so:
funcM_ = fst `fmap` funcM -- error
I could add back all the points, but it seems like there should be something I could replace fmap with so that the above will work. Kind of like replacing ($) with (.) would make this work in a pure context.
What is the function I am looking for?
Since you’re composing a one-argument function (fmap) with a three-argument function (funcM), you need three levels of composition:
funcM_ = ((fmap fst .) .) . funcM
This is equivalent to the pointed version by a simple expansion:
funcM_ x = (fmap fst .) . funcM x
funcM_ x y = fmap fst . funcM x y
funcM_ x y z = fmap fst (funcM x y z)
This follows from the type of fmap, really:
fmap :: (Functor f) => (a -> b) -> f a -> f b
You’re just partially applying the arguments to funcM so that you have an f a (here IO (x, y)) which you give to fmap fst to get back an f b (IO x).
As an aside, M_ usually implies returning m ().
Take a look at the following answer: https://stackoverflow.com/a/20279307/783743 It explains how to convert your code into pointfree style. Let's start with a non-pointfree definition of funcM_:
funcM_ a b c = fmap fst (funcM a b c)
-- But `\x -> f (g x)` is `f . g`. Hence:
funcM_ a b = fmap fst . (funcM a b)
-- But `\x -> f (g x)` is `f . g`. Hence:
funcM_ a = (fmap fst .) . (funcM a)
-- But `\x -> f (g x)` is `f . g`. Hence:
funcM_ = ((fmap fst .) .) . funcM
Another way to do this would be to use uncurry and curry as follows:
uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d
uncurry3 f (a, b, c) = f a b c
curry3 :: ((a, b, c) -> d) -> a -> b -> c -> d
curry3 f a b c = f (a, b, c)
(.::) :: (d -> e) -> (a -> b -> c -> d) -> a -> b -> c -> e
f .:: g = curry3 (f . (uncurry3 g))
Now you can write funcM_ as follows:
funcM_ = fmap fst .:: funcM
You could also write .:: in pointfree style as follows:
(.::) :: (d -> e) -> (a -> b -> c -> d) -> a -> b -> c -> e
(.::) = (.) . (.) . (.)
Hope that helped.
Add a dot for each argument to funcM
These are all equivalent:
((fmap fst . ) .) . funcM
((.) . (.) . (.)) (fmap fst) funcM
(fmap . fmap . fmap) (fmap fst) funcM
import Data.Functor.Syntax -- from 'functors' package
(.::) (fmap fst) funcM
Note that all I did was change the implicit ($) to (.).
:-)
(.) is the implementation of fmap for the function instance of Functor :
instance Functor ((->) a) b where
fmap f g = f . g
GHCi :t is your friend.

Haskell function composition, type of (.)(.) and how it's presented

So i know that:
(.) = (f.g) x = f (g x)
And it's type is (B->C)->(A->B)->A->C
But what about:
(.)(.) = _? = _?
How this is represented? I thought of:
(.)(.) = (f.g)(f.g)x = f(g(f(g x))) // this
(.)(.) = (f.g.h)x = f(g(h x)) // or this
But as far as i tried to get type of it, it's not correct to what GHCi tells me.
So what are both "_?"
Also - what does function/operator $ do?
First off, you're being sloppy with your notation.
(.) = (f.g) x = f (g x) -- this isn't true
What is true:
(.) f g x = (f.g) x = f (g x)
(.) = \f g x -> f (g x)
And its type is given by
(.) :: (b -> c) -> (a -> b) -> a -> c
-- n.b. lower case, because they're type *variables*
Meanwhile
(.)(.) :: (a -> b -> d) -> a -> (c -> b) -> c -> d
-- I renamed the variables ghci gave me
Now let's work out
(.)(.) = (\f' g' x' -> f' (g' x')) (\f g x -> f (g x))
= \g' x' -> (\f g x -> f (g x)) (g' x')
= \g' x' -> \g x -> (g' x') (g x)
= \f y -> \g x -> (f y) (g x)
= \f y g x -> f y (g x)
= \f y g x -> (f y . g) x
= \f y g -> f y . g
And ($)?
($) :: (a -> b) -> a -> b
f $ x = f x
($) is just function application. But whereas function application via juxtaposition is high precedence, function application via ($) is low precedence.
square $ 1 + 2 * 3 = square (1 + 2 * 3)
square 1 + 2 * 3 = (square 1) + 2 * 3 -- these lines are different
As dave4420 mentions,
(.) :: (b -> c) -> (a -> b) -> a -> c
So what is the type of (.) (.)? dave4420 skips that part, so here it is: (.) accepts a value of type b -> c as its first argument, so
(.) :: ( b -> c ) -> (a -> b) -> a -> c
(.) :: (d -> e) -> ((f -> d) -> f -> e)
so we have b ~ d->e and c ~ (f -> d) -> f -> e, and the resulting type of (.)(.) is (a -> b) -> a -> c. Substituting, we get
(a -> d -> e) -> a -> (f -> d) -> f -> e
Renaming, we get (a -> b -> c) -> a -> (d -> b) -> d -> c. This is a function f that expects a binary function g, a value x, a unary function h and another value y:
f g x h y = g x (h y)
That's the only way this type can be realized: g x :: b -> c, h y :: b and so g x (h y) :: c, as needed.
Of course in Haskell, a "unary" function is such that expects one or more arguments; similarly a "binary" function is such that expects two or more arguments. But not less than two (so using e.g. succ is out of the question).
We can also tackle this by writing equations, combinators-style1. Equational reasoning is easy:
(.) (.) x y z w q =
((.) . x) y z w q =
(.) (x y) z w q =
(x y . z) w q =
x y (z w) q
We just throw as much variables as needed into the mix and then apply the definition back and forth. q here was an extra, so we can throw it away and get the final definition,
_BB x y z w = x y (z w)
(coincidentally, (.) is known as B-combinator).
1 a b c = (\x -> ... body ...) is equivalent to a b c x = ... body ..., and vice versa, provided that x does not appear among {a,b,c}.

How can I understand "(.) . (.)"?

I believe I understand fmap . fmap for Functors, but on functions it's hurting my head for months now.
I've seen that you can just apply the definition of (.) to (.) . (.), but I've forgot how to do that.
When I try it myself it always turns out wrong:
(.) f g = \x -> f (g x)
(.) (.) (.) = \x -> (.) ((.) x)
\x f -> (.) ((.) x) f
\x f y -> (((.)(f y)) x)
\x f y g-> (((.)(f y) g) x)
\x f y g-> ((f (g y)) x)
\x f y g-> ((f (g y)) x):: t2 -> (t1 -> t2 -> t) -> t3 -> (t3 -> t1) -> t
If "just applying the definition" is the only way of doing it, how did anybody come up with (.) . (.)?
There must be some deeper understanding or intuition I'm missing.
Coming up with (.) . (.) is actually pretty straightforward, it's the intuition behind what it does that is quite tricky to understand.
(.) gets you very far when rewriting expression into the "pipe" style computations (think of | in shell). However, it becomes awkward to use once you try to compose a function that takes multiple arguments with a function that only takes one. As an example, let's have a definition of concatMap:
concatMap :: (a -> [b]) -> [a] -> [b]
concatMap f xs = concat (map f xs)
Getting rid of xs is just a standard operation:
concatMap f = concat . map f
However, there's no "nice" way of getting rid of f. This is caused by the fact, that map takes two arguments and we'd like to apply concat on its final result.
You can of course apply some pointfree tricks and get away with just (.):
concatMap f = (.) concat (map f)
concatMap f = (.) concat . map $ f
concatMap = (.) concat . map
concatMap = (concat .) . map
But alas, readability of this code is mostly gone. Instead, we introduce a new combinator, that does exactly what we need: apply the second function to the final result of first one.
-- .: is fairly standard name for this combinator
(.:) :: (c -> d) -> (a -> b -> c) -> a -> b -> d
(f .: g) x y = f (g x y)
concatMap = concat .: map
Fine, that's it for motivation. Let's get to the pointfree business.
(.:) = \f g x y -> f (g x y)
= \f g x y -> f ((g x) y)
= \f g x y -> f . g x $ y
= \f g x -> f . g x
Now, here comes the interesting part. This is yet another of the pointfree tricks that usually helps when you get stuck: we rewrite . into its prefix form and try to continue from there.
= \f g x -> (.) f (g x)
= \f g x -> (.) f . g $ x
= \f g -> (.) f . g
= \f g -> (.) ((.) f) g
= \f -> (.) ((.) f)
= \f -> (.) . (.) $ f
= (.) . (.)
As for intuition, there's this very nice article that you should read. I'll paraphrase the part about (.):
Let's think again about what our combinator should do: it should apply f to the result of result of g (I've been using final result in the part before on purpose, it's really what you get when you fully apply - modulo unifying type variables with another function type - the g function, result here is just application g x for some x).
What it means for us to apply f to the result of g? Well, once we apply g to some value, we'll take the result and apply f to it. Sounds familiar: that's what (.) does.
result :: (b -> c) -> ((a -> b) -> (a -> c))
result = (.)
Now, it turns out that composition (our of word) of those combinators is just a function composition, that is:
(.:) = result . result -- the result of result
You can also use your understanding of fmap . fmap.
If you have two Functors foo and bar, then
fmap . fmap :: (a -> b) -> foo (bar a) -> foo (bar b)
fmap . fmap takes a function and produces an induced function for the composition of the two Functors.
Now, for any type t, (->) t is a Functor, and the fmap for that Functor is (.).
So (.) . (.) is fmap . fmap for the case where the two Functors are (->) s and (->) t, and thus
(.) . (.) :: (a -> b) -> ((->) s) ((->) t a) -> ((->) s) ((->) t b)
= (a -> b) -> (s -> (t -> a)) -> (s -> (t -> b))
= (a -> b) -> (s -> t -> a ) -> (s -> t -> b )
it "composes" a function f :: a -> b with a function of two arguments, g :: s -> t -> a,
((.) . (.)) f g = \x y -> f (g x y)
That view also makes it clear that, and how, the pattern extends to functions taking more arguments,
(.) :: (a -> b) -> (s -> a) -> (s -> b)
(.) . (.) :: (a -> b) -> (s -> t -> a) -> (s -> t -> b)
(.) . (.) . (.) :: (a -> b) -> (s -> t -> u -> a) -> (s -> t -> u -> b)
etc.
Your solution diverges when you introduce y. It should be
\x f y -> ((.) ((.) x) f) y :: (c -> d) -> (a -> b -> c) -> a -> b -> d
\x f y z -> ((.) ((.) x) f) y z :: (c -> d) -> (a -> b -> c) -> a -> b -> d
\x f y z -> ((.) x (f y)) z :: (c -> d) -> (a -> b -> c) -> a -> b -> d
-- Or alternately:
\x f y z -> (x . f y) z :: (c -> d) -> (a -> b -> c) -> a -> b -> d
\x f y z -> (x (f y z)) :: (c -> d) -> (a -> b -> c) -> a -> b -> d
Which matches the original type signature: (.) . (.) :: (c -> d) -> (a -> b -> c) -> a -> b -> d
(It's easiest to do the expansion in ghci, where you can check each step with :t expression)
Edit:
The deeper intution is this:
(.) is simply defined as
\f g -> \x -> f (g x)
Which we can simplify to
\f g x -> f (g x)
So when you supply it two arguments, it's curried and still needs another argument to resolve.
Each time you use (.) with 2 arguments, you create a "need" for one more argument.
(.) . (.) is of course just (.) (.) (.), so let's expand it:
(\f0 g0 x0 -> f0 (g0 x0)) (\f1 g1 x1 -> f1 (g1 x1)) (\f2 g2 x2 -> f2 (g2 x2))
We can beta-reduce on f0 and g0 (but we don't have an x0!):
\x0 -> (\f1 g1 x1 -> f1 (g1 x1)) ((\f2 g2 x2 -> f2 (g2 x2)) x0)
Substitute the 2nd expression for f1...
\x0 -> \g1 x1 -> ((\f2 g2 x2 -> f2 (g2 x2)) x0) (g1 x1)
Now it "flips back"! (beta-reduction on f2):
This is the interesting step - x0 is substituted for f2 -- This means that x, which could have been data, is instead a function.
This is what (.) . (.) provides -- the "need" for the extra argument.
\x0 -> \g1 x1 -> (\g2 x2 -> x0 (g2 x2)) (g1 x1)
This is starting to look normal...
Let's beta-reduce a last time (on g2):
\x0 -> \g1 x1 -> (\x2 -> x0 ((g1 x1) x2))
So we're left with simply
\x0 g1 x1 x2 -> x0 ((g1 x1) x2)
, where the arguments are nicely still in order.
So, this is what I get when I do a slightly more incremental expansion
(.) f g = \x -> f (g x)
(.) . g = \x -> (.) (g x)
= \x -> \y -> (.) (g x) y
= \x -> \y -> \z -> (g x) (y z)
= \x y z -> (g x) (y z)
(.) . (.) = \x y z -> ((.) x) (y z)
= \x y z -> \k -> x (y z k)
= \x y z k -> x (y z k)
Which, according to ghci has the correct type
Prelude> :t (.) . (.)
(.) . (.) :: (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c
Prelude> :t \x y z k -> x (y z k)
\x y z k -> x (y z k)
:: (t1 -> t) -> (t2 -> t3 -> t1) -> t2 -> t3 -> t
Prelude>
While I don't know the origins of this combinator, it is likely that it was
developed for use in combinatory logic, where you work strictly with combinators,
so you can't define things using more convenient lambda expressions. There may be
some intuition that goes with figuring these things out, but I haven't found it.
Most likely, you would develop some level of intuition if you had to do it enough.
It's easiest to write equations, combinators-style, instead of lambda-expressions: a b c = (\x -> ... body ...) is equivalent to a b c x = ... body ..., and vice versa, provided that x does not appear among {a,b,c}. So,
-- _B = (.)
_B f g x = f (g x)
_B _B _B f g x y = _B (_B f) g x y
= (_B f) (g x) y
= _B f (g x) y
= f ((g x) y)
= f (g x y)
You discover this if, given f (g x y), you want to convert it into a combinatory form (get rid of all the parentheses and variable repetitions). Then you apply patterns corresponding to the combinators' definitions, hopefully tracing this derivation backwards. This is much less mechanical/automatic though.

Haskell: Why is ((.).(.)) f g equal to f . g x?

Could you please explain the meaning of the expression ((.).(.))?
As far as I know (.) has the type (b -> c) -> (a -> b) -> a -> c.
(.) . (.) is the composition of the composition operator with itself.
If we look at
((.) . (.)) f g x
we can evaluate that a few steps, first we parenthesise,
((((.) . (.)) f) g) x
then we apply, using (foo . bar) arg = foo (bar arg):
~> (((.) ((.) f)) g) x
~> (((.) f) . g) x
~> ((.) f) (g x)
~> f . g x
More principled,
(.) :: (b -> c) -> (a -> b) -> (a -> c)
So, using (.) as the first argument of (.), we must unify
b -> c
with
(v -> w) -> (u -> v) -> (u -> w)
That yields
b = v -> w
c = (u -> v) -> (u -> w)
and
(.) (.) = ((.) .) :: (a -> v -> w) -> a -> (u -> v) -> (u -> w)
Now, to apply that to (.), we must unify the type
a -> v -> w
with the type of (.), after renaming
(s -> t) -> (r -> s) -> (r -> t)
which yields
a = s -> t
v = r -> s
w = r -> t
and thus
(.) . (.) :: (s -> t) -> (u -> r -> s) -> (u -> r -> t)
and from the type we can (almost) read that (.) . (.) applies a function (of one argument) to the result of a function of two arguments.
You've got an answer already, here's a slightly different take on it.
In combinatory logic (.) is B-combinator : Babc = a(bc). When writing combinator expressions it is customary to assume that every identifier consists of one letter only, and omit white-space in application, to make the expressions more readable. Of course the usual currying applies: abcde is (((ab)c)d)e and vice versa.
(.) is B, so ((.) . (.)) == (.) (.) (.) == BBB. So,
BBBfgxy = B(Bf)gxy = (Bf)(gx)y = Bf(gx)y = (f . g x) y
abc a bc a b c
We can throw away both ys at the end (this is known as eta-reduction: Gy=Hy --> G=H, if y does not appear inside H1). But also, another way to present this, is
BBBfgxy = B(Bf)gxy = ((f .) . g) x y = f (g x y) -- (.) f == (f .)
-- compare with: (f .) g x = f (g x)
((f .) . g) x y might be easier to type in than ((.).(.)) f g x y, but YMMV.
1 For example, with S combinator, defined as Sfgx = fx(gx), without regard for that rule we could write
Sfgx = fx(gx) = B(fx)gx = (f x . g) x
Sfg = B(fx)g = (f x . g) --- WRONG, what is "x"?
which is nonsense.

Resources