We continue our series of articles on functional F # programming. Today we will talk about the associativity and composition of functions, as well as compare the composition and the pipeline. Look under the cat!

Associativity and composition of functions
Associativity of functions
Suppose there is a chain of functions written in a row. In what order will they be combined?
For example, what does this feature mean?
let F xyz = xyz
Does this mean that the function y
should be applied to the argument z
, and then the result should be passed to x
? Those.:
let F xyz = x (yz)
Or is the function x
applied to the argument y
, after which the function obtained as a result will be evaluated with the argument z
? Those.:
let F xyz = (xy) z
- True to the second option.
- The use of functions has left associativity .
xyz
means the same as (xy) z
.- And
wxyz
is ((wx) y) z
. - This should not look amazing.
- We have already seen how partial application works.
- If we talk about
x
as a function with two parameters, then (xy) z
is the result of the partial application of the first parameter, followed by the transfer of the argument z
to the intermediate function.
If you need the right associativity, you can use brackets or pipe. The following three entries are equivalent:
let F xyz = x (yz) let F xyz = yz |> x // let F xyz = x <| yz //
As an exercise, try to display the signatures of these functions without real computation.
Composition of functions
We have mentioned the composition of functions several times, but what does this term really mean? It seems daunting at first glance, but in reality everything is quite simple.
Let's say we have a function "f" that matches the type "T1" to the type "T2". We also have the function "g", which converts the type "T2" to the type "T3". Then we can connect the output "f" and input "g" by creating a new function that converts the type "T1" to the type "T3".

For example:
let f (x:int) = float x * 3.0 // f - int->float let g (x:float) = x > 4.0 // g - float->bool
We can create a new function "h", which takes the output "f" and uses it as input for "g".
let h (x:int) = let y = f(x) g(y) // g
Slightly more compact:
let h (x:int) = g ( f(x) ) // h int->bool // h 1 h 2
So far, so simple. This is interesting, we can define a new function "compose", which takes the functions "f" and "g" and combines them without even knowing their signatures.
let compose fgx = g ( f(x) )
After execution, you can see that the compiler correctly decided that " f
" is a function of the generalized type 'a
to the generalized type 'b
, and the ' g
' is limited to entering the type 'b
:
val compose : ('a -> 'b) -> ('b -> 'c) -> 'a -> 'c
(Note that the generalized composition of operations is possible only because each function has exactly one input parameter and one conclusion. This approach is impossible in non-functional languages.)
As we see, this definition is used for the operator " >>
".
let (>>) fgx = g ( f(x) )
Thanks to this definition, it is possible to build new functions based on existing ones with the help of composition.
let add1 x = x + 1 let times2 x = x * 2 let add1Times2 x = (>>) add1 times2 x // add1Times2 3
Explicit recording is quite cumbersome. But you can make its use easier to understand.
First, you can get rid of the parameter x
, and the composition will return partial application.
let add1Times2 = (>>) add1 times2
Secondly, because >>
is a binary operator, you can put it in the center.
let add1Times2 = add1 >> times2
The use of the composition makes the code cleaner and clearer.
let add1 x = x + 1 let times2 x = x * 2 // let add1Times2 x = times2(add1 x) // let add1Times2 = add1 >> times2
Using the composition operator in practice
The composition operator (like all infix operators) has a lower priority than normal functions. This means that the functions used in the composition can have arguments without using parentheses.
For example, if the functions "add" and "times" have parameters, they can be transferred during composition.
let add nx = x + n let times nx = x * n let add1Times2 = add 1 >> times 2 let add5Times3 = add 5 >> times 3 // add5Times3 1
As long as the corresponding inputs and outputs of the functions match, the functions can use any values. For example, consider the following code that performs a function twice:
let twice f = f >> f // ('a -> 'a) -> ('a -> 'a)
Note that the compiler infers that f
accepts and returns values of the same type.
Now consider the function " +
". As we saw earlier, input is int
, but output is actually (int->int)
. Thus, " +
" can be used in " twice
". Therefore, you can write:
let add1 = (+) 1 // (int -> int) let add1Twice = twice add1 // (int -> int) // add1Twice 9
On the other hand, you cannot write:
let addThenMultiply = (+) >> (*)
Because the input "*" must be an int
, not an int->int
function (which is the output of the addition).
But if you tweak the first function so that it returns only an int
, everything will work:
let add1ThenMultiply = (+) 1 >> (*) // (+) 1 (int -> int) 'int' // add1ThenMultiply 2 7
Composition can also be performed in reverse order with " <<
", if necessary:
let times2Add1 = add 1 << times 2 times2Add1 3
The reverse composition is mainly used to make the code more similar to English ("English-like"). For example:
let myList = [] myList |> List.isEmpty |> not // myList |> (not << List.isEmpty) //
Composition vs. conveyor
You may be confused by the small difference between the composition and the conveyor, as they may look very similar.
First, look at the definition of a pipeline:
let (|>) xf = fx
All this allows you to put the arguments of functions before it, and not after. That's all. If the function has several parameters, then the input must be the last parameter (in the current parameter set, and not in general). An example encountered earlier:
let doSomething xyz = x+y+z doSomething 1 2 3 // 3 |> doSomething 1 2 //
The composition is not the same and can not be a substitute for the pipe. In the following example, even the number 3 is not a function, so the "output" cannot be passed to doSomething
:
3 >> doSomething 1 2 // // f >> g g(f(x)) : doSomething 1 2 ( 3(x) ) // 3 ! // error FS0001: This expression was expected to have type 'a->'b // but here has type int
The compiler complains that the value "3" should be a kind of the 'a->'b
functions.
Compare this with the definition of a composition that takes 3 arguments, where the first two should be functions.
let (>>) fgx = g ( f(x) ) let add nx = x + n let times nx = x * n let add1Times2 = add 1 >> times 2
Attempts to use the pipeline instead of composition will result in a compilation error. In the following example, " add 1
" is the (partial) function int->int
, which cannot be used as the second parameter for " times 2
".
let add1Times2 = add 1 |> times 2 // // x |> f f(x) : let add1Times2 = times 2 (add 1) // add1 'int' // error FS0001: Type mismatch. 'int -> int' does not match 'int'
The compiler will complain that " times 2
" needs to accept the int->int
parameter, i.e. be a function (int->int)->'a
.
Additional resources
For F #, there are many tutorials, including materials for those who come with C # or Java experience. The following links may be helpful as you learn more about F #:
Several other ways to get started with learning F # are also described.
Finally, the F # community is very friendly to beginners. There is a very active Slack chat, supported by the F # Software Foundation, with rooms for beginners that you can freely join . We strongly recommend that you do this!
Do not forget to visit the site of the Russian-speaking community F # ! If you have any questions about learning the language, we will be happy to discuss them in chat rooms:
About authors of translation
Translated by @kleidemos
Translation and editorial changes are made by the efforts of the Russian-speaking community of F # -developers . We also thank @schvepsss and @shwars for preparing this article for publication.