Hi, I’ve played a little bit around in the repl to understand the difference between the compose operator >> vs the forward pipe operator |>. Not from a usage perspective, but from the highlighted in bold difference in their type signatures. Why don’t they boil down to the same signature. What does the parenthesis really mean, searching the documentation it appears to be the signature of a HOF, and if that’s the case, why is add1 not a reported as such when it works. Can anyone elaborate on the differece - if any - on add1 and add2. Many thanks. Z.
let add x y = x+y;;
val add : x:int → y:int → int
let add1 x = x |> add 3 |> add 2;;
val add1 : x:int → int
let add2 = add 3 >> add 2;;
val add2 : (int → int)
let applyHOF f x = f x;;
val applyHOF : f:('a → 'b) → x:'a → 'b
applyHOF add1 3;;
val it : int = 8
applyHOF add2 3;;
val it : int = 8
The reason why the signatures look different is:
-
add1
has a named parameter, so it’s a part of the signature
-
add2
is a function value with a “hidden parameter” embedded in it
The first part is easy to explain. If there’s a named parameter to a function, F# tooling will display it.
The second one is about F# tooling calling out a subtle distinction even if the semantics are the same. As you can clearly see, both add1
and add2
satisfy the signature for applyHOF
. They are equivalent in terms of their types. But they are subtly different: add1
is the name of a function with an explicit parameter, whereas add2
is the name of a value that is a function, whose signature accepts a single int
and produces another one.
Syntactically they are clearly not the same constructs. But semantically they end up being equivalent. Somewhere along the way information gets added or lost (or both) to make that the case. At which point along that pathway do you feel that tooling which reports signatures of constructs should act? In this case, F# tooling has chosen to be more on the “early” side of things to distinguish between the two.
N.B. just because add1
and add2
are the same semantically doesn’t mean that they’re actually equivalent. In this case, add2
allocates more than add1
because it composes two partially-applied functions. The information for that data currently must be “stored” in objects. In the case of add1
, it’s quite trivial for the compiler to analyze the structure of the function and understand that it can emit as a simple static method with constants thrown in. See generated code for more info
Thank you for excellent thorough explanation.