HOF type signature confusion, Forward Pipe operator vs. compose operator

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.