Prince M
Prince M

Reputation: 441

F#: Piping vs. Composing vs. ... Composing?

I am new to everything - F#, programming in general, and this community. I am a mathematician with brief exposure to computer science in my undergrad. I am trying to accomplish some tasks in F# and the "F# Cheat Sheet" exhibits what appears to be three different ways to compose functions without explaining the repetition. Here is the relevant information from the link to see what I mean.

The let keyword also defines named functions.

let negate x = x * -1 
let square x = x * x 
let print x = printfn "The number is: %d" x

let squareNegateThenPrint x = 
print (negate (square x)) 

Pipe operator |> is used to chain functions and arguments together. Double-backtick identifiers are handy to improve readability especially in unit testing:

let ``square, negate, then print`` x = 
    x |> square |> negate |> print

Composition operator >> is used to compose functions:

let squareNegateThenPrint' = 
    square >> negate >> print

By inspection and by playing in VS F# interactive with the functions:

  1. squareNegateThenPrint x
  2. ``square, negate, then print'' x
  3. squareNegateThenPrint'

it appears that this is a list of 3 ways to accomplish the exact same thing, are there any nuances here? I am convinced that given the same int they will all return the same int, but how about beyond that? What am I not seeing? What are advantages and disadvantages of each of the three methods?

2 and 3 both use 'operators' and 1 seems to be the usual 'mathematical' way of composing functions to make a new function from old functions. I suspect option 3 to be truly equivalent to 1 (in the sense that the >> operator is defined so that square >> negate >> print is actually computed as print (negate (square x)) but the code benefits in readability since you see the function names in the order they happen instead of reverse order with the usual mathematical notation, and defining this way saves you a keystroke or two since you don't have to include the x at the end of the function name since the >> operator probably makes the left function automatically inherit dependence on the variable of the function on the right, without explicit reference to the variable.

But then how does the piping method play into this? Is the piping operator a more general operator that just so happens to work for function composition?

Also, I did google quite a bit and try to read the documentation before posting but I was not getting anywhere. I am sure if I just moved on and kept learning the language, sometime in the next year I would understand the differences. But I am also confident someone on here can expedite that process and explain or provide some nice examples. Lastly, I am not proficient in C#, or really any other language (except mathematics) so explanations for a total noob and not just an f# noob appreciated. Thanks!

Upvotes: 24

Views: 4327

Answers (2)

Fyodor Soikin
Fyodor Soikin

Reputation: 80915

First of all - yes, all of these ways are equivalent both "logically" and when compiled down to the hardware. This is because the |> and >> operators are defined as inline. The definition looks roughly like this:

let inline (|>) x f = f x
let inline (>>) f g = fun x -> g (f x)

The meaning of the inline keyword is that the compiler will replace calls to the function with the body of the function, and then compile the result. Therefore, both of the following:

x |> f |> g
(f >> g) x

will be compiled exactly the same way as the following:

g (f x)

In practice, however, there are gotchas.


One gotcha is with type inference and its interplay with classes/interfaces. Consider the following:

let b = "abcd" |> (fun x -> x.Length)
let a = (fun x -> x.Length) "abcd"

Even though these definitions are equivalent, both logically and in compiled form, yet the first definition will compile, and the second will not. This happens because type inference in F# proceeds left-to-right without doublebacks, and therefore, in the first definition, by the time the compiler gets to x.Length, it already knows that x is a string, so it can resolve member lookup correctly. In the second example, the compiler doesn't know what x is, because it hasn't encountered the argument "abcd" yet.


Another gotcha has to do with the Dreaded Value Restriction. In simple terms it says that a definition that is syntactically (not logically!) a value (as opposed to a function) cannot be generic. This has obscure reasons that have to do with mutability - see the linked article for explanation.

Applying this to function composition, consider the following code (note that both f and g are generic functions):

let f x = [x]
let g y = [y]

let h1 = f >> g
let h2 x = x |> f |> g

Here, h2 will compile fine, but h1 will not, complaining about the Value Restriction.


In practice, the choice between these three ways usually comes down to readability and convenience. None of these is inherently better than the others. As I write code, I usually choose just based on my taste.

Upvotes: 28

Aaron M. Eshbach
Aaron M. Eshbach

Reputation: 6510

These are all basically equivalent concepts that are used in different circumstances. There isn't a right-way or wrong-way that will be universally applicable, but there are times when you can take advantage of the pipelining and composition operators that become apparent through practice and more exposure to F# programming patterns.

To give a few examples, pipelining is often used when working with sequences, as it allows very long chains of operations to be composed together in a readable way that looks something like a fluent-style query syntax.

[0..100]
|> List.filter (fun x -> x % 2 = 0)
|> List.map (fun x -> x / 2)
|> List.sum

For many of us who use F# all the time, this is much more readable than something like List.sum (List.map (fun x -> x / 2) (List.filter (fun x -> x % 2 = 0) [0..100])).

Composition is often used when working with higher-order functions such as bind. For example:

[0..5] 
|> List.tryFind (fun x -> x = 3) 
|> Option.bind ((*) 3 >> Some)

Here, we're using pipeling to do a tryFind on a list and to pipe the Option type it returns to Option.bind That takes a function with the signature int -> Option 'b, but if we already have a function int -> int (like multiply) we can use >> to compose that function with Some and pass the composed function to bind. ((*) 3 is just the F# syntax for partially applying 3 to the multiply function, returning a function that multiplies any integer by 3).

We could achieve the same thing by writing Option.bind (fun x -> Some (x * 3)), but the >> operator and the partial-application syntax save us some keystrokes.

Upvotes: 6

Related Questions