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:
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!
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 the 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 the code, I usually choose just based on my taste.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With