Graham Gayler
Graham Gayler

Reputation: 19

F# pipe operator confusion

I am learning F# and the use cases of the |>, >>, and << operators confuse me. I get that everything if statements, functions, etc. act like variables but how do these work?

Upvotes: 2

Views: 1226

Answers (2)

Romain Deneau
Romain Deneau

Reputation: 3061

Pipes and composition operators have simple definition but are difficult to grasp. But once we have understand them, they are super useful and we miss them when we get back to C#.

Here some explanations but you get the best feedbacks from your own experiments. Have fun!

Pipe right operator |>

val |> fnfn val

Utility:

  • Building a pipeline, to chain calls to functions: x |> f |> gg (f x).
    • Easier to read: just follow the data flow
    • No intermediary variables
  • Natural language in english: Subject Verb.
    • It's regular in object-oriented code : myObject.do()
    • In F#, the "subject" is usually the last parameter: List.map f list. Using |>, we get back the natural "Subject Verb" order: list |> List.map f
  • Final benefit but not the least: help type inference:
let items = ["a"; "bb"; "ccc"]

let longestKo = List.maxBy (fun x -> x.Length) items  // ❌ Error FS0072
//                                   ~~~~~~~~

let longest = items |> List.maxBy (fun x -> x.Length) // ✅ return "ccc"

Pipe left operator <|

fn <| expressionfn (expression)

  • Less used than |>
  • ✅ Small benefit: avoiding parentheses
  • ❌ Major drawback: inverse of the english natural "left to right" reading order and inverse of execution order (because of left-associativity)
printf "%i" 1+2          // 💥 Error
printf "%i" (1+2)        // With parentheses
printf "%i" <| 1+2       // With pipe left

What about this kind of expression: x |> fn <| y

  • In theory, allow using fn in infix position, equivalent of fn x y
  • In practice, it can be very confusing for some readers not used to it.

👉 It's probably better to avoid using <|

Forward composition operator >>

Binary operator placed between 2 functions:
f >> gfun x -> g (f x)fun x -> x |> f |> g

Result of the 1st function is used as argument for the 2nd function
→ types must match: f: 'T -> 'U and g: 'U -> 'Vf >> g :'T -> 'V

let add1 x = x + 1
let times2 x = x * 2

let add1Times2 x = times2(add1 x) // 😕 Style explicit but heavy
let add1Times2' = add1 >> times2  // 👍 Style concise

Backward composition operator <<

f >> gg << f

Less used than >>, except to get terms in english order:

let even x = x % 2 = 0

// even not 😕
let odd x = x |> even |> not

// "not even" is easier to read 👍
let odd = not << even

Note: << is the mathematical function composition : g ∘ ffun x -> g (f x)g << f.
It's confusing in F# because it's >> that is usually called the "composition operator" ("forward" being usually omitted).

On the other hand, the symbols used for these operators are super useful to remember the order of execution of the functions: f >> g means apply f then apply g. Even if argument is implicit, we get the data flow direction:

  • >> : from left to right → f >> gfun x -> x |> f |> g
  • << : from right to left → f << gfun x -> f <| (g <| x)

(Edited after good advices from David)

Upvotes: 2

David Raab
David Raab

Reputation: 4488

Usually we (community) say the Pipe Operator |> is just a way, to write the last argument of a function before the function call. For example

f x y

can be written

y |> f x

but for correctness, this is not true. It just pass the next argument to a function. So you could even write.

y |> (x |> f)

All of this, and all other kind of operators works, because in F# all functions are curried by default. This means, there exists only functions with one argument. Functions with many arguments, are implemented that a functions return another function.

You could also write

(f x) y

for example. The function f is a function that takes x as argument and returns another function. This then gets y passed as an argument.

This process is automatically done by the language. So if you write

let f x y z = x + y + z

it is the same as:

let f = fun x -> fun y -> fun z -> x + y + z

Currying is by the way the reason why parenthesis in a ML-like language are not enforced compared to a LISP like language. Otherwise you would have needded to write:

(((f 1) 2) 3)

to execute a function f with three arguments.

The pipe operator itself is just another function, it is defined as

let (|>) x f = f x

It takes a value x as its first argument. And a function f as its second argument. Because operators a written "infix" (this means between two operands) instead of "prefix" (before arguments, the normal way), this means its left argument to the operator is the first argument.

In my opinion, |> is used too much by most F# people. It makes sense to use piping if you have a chain of operations, one after another. Typically for example if you have multiple list operations.

Let's say, you want to square all numbers in a list and then filter only the even ones. Without piping you would write.

List.filter isEven (List.map square [1..10])

Here the second argument to List.filter is a list that is returned by List.map. You can also write it as

List.map square [1..10]
|> List.filter isEven

Piping is Function application, this means, you will execute/run a function, so it computes and returns a value as its result.

In the above example List.map is first executed, and the result is passed to List.filter. That's true with piping and without piping. But sometimes, you want to create another function, instead of executing/running a function. Let's say you want to create a function, from the above. The two versions you could write are

let evenSquares xs = List.filter isEven (List.map square xs)
let evenSquares xs = List.map square xs |> List.filter isEven

You could also write it as function composition.

let evenSquares = List.filter isEven << List.map square
let evenSquares = List.map square >> List.filter isEven

The << operator resembles function composition in the "normal" way, how you would write a function with parenthesis. And >> is the "backwards" compositon, how it would be written with |>.

The F# documentation writes it the other way, what is backward and forward. But i think the F# language creators are wrong.

The function composition operators are defined as:

let (<<) f g x = f (g x)
let (>>) f g x = g (f x)

As you see, the operator has technically three arguments. But remember currying. When you write f << g, then the result is another functions, that expects the last argument x. Passing less arguments then needed is also often called Partial Application.

Function composition is less often used in F#, because the compiler sometimes have problems with type inference if the function arguments are generic.

Theoretically you could write a program without ever defining a variable, just through function composition. This is also named Point-Free style.

I would not recommend it, it often makes code harder to read and/or understand. But it is sometimes used if you want to pass a function to another Higher-Order function. This means, a functions that take another function as an argument. Like List.map, List.filter and so on.

Upvotes: 5

Related Questions