I’m taking my first baby-steps in learning functional programing using F# and I’ve just come across the Forward Pipe (|>) and Forward Composition (>>) operators. At first I thought they were just sugar rather than having an effect on the final running code (though I know piping helps with type inference).
However I came across this SO article: What are advantages and disadvantages of “point free” style in functional programming? Which has two interesting and informative answers (that instead of simplifying things for me opened a whole can of worms surrounding “point-free” or “pointless” style) My take-home from these (and other reading around) is that point-free is a debated area. Like lambas, point-free style can make code easier to understand, or much harder, depending on use. It can help in naming things meaningfully.
But my question concerns a comment on the first answer: AshleyF muses in the answer:
“It seems to me that composition may reduce GC pressure by making it more obvious to the compiler that there is no need to produce intermediate values as in pipelining; helping make the so-called "deforestation" problem more tractable.”
Gasche replies:
“The part about improved compilation is not true at all. In most languages, point-free style will actually decrease performances. Haskell relies heavily on optimizations precisely because it's the only way to make the cost of these things bearable. At best, those combinators are inlined away and you get an equivalent pointful version”
Can anyone expand on the performance implications? (In general and specifically for F#) I had just assumed it was a writing-style thing and the compiler would unstrangle both idioms into equivalent code.
This answer is going to be F#-specific. I don't know how the internals of other functional languages work, and the fact that they don't compile to CIL could make a big difference.
I can see three questions here:
|>
?>>
?The answers (using examples from the question you linked to):
Is there any difference between x |> sqr |> sum
and sum (sqr x)
?
No, there isn't. The compiled CIL is exactly the same (here represented in C#):
sum.Invoke(sqr.Invoke(x))
(Invoke()
is used, because sqr
and sum
are not CIL methods, they are FSharpFunc
, but that's not relevant here.)
Is there any difference between (sqr >> sum) x
and sum (sqr x)
?
No, both samples compile to the same CIL as above.
Is there any difference between let sumsqr = sqr >> sum
and let sumsqr x = (sqr >> sum) x
?
Yes, the compiled code is different. If you specify the argument, sumsqr
is compiled into a normal CLI method. But if you don't specify it, it's compiled as a property of type FSharpFunc
with a backing field, whose Invoke()
method contains the code.
But the effect of all is that invoking the point-free version means loading one field (the FSharpFunc
), which is not done if you specify the argument. But I think that shouldn't measurably affect performance, except in the most extreme circumstances.
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