4

Edit: fix in this post stands for a fixed-point combination written down in Haskell in general, not just Data.Function.fix.

It is widely known that fix could be non-sharing as GHC does not always eliminate common subexpressions:

Long story short: "If you care about CSE, do it by hand."

fix could be written down point-free pretty easily with the use of the S-combinator:

fix = ($) <*> fix

If we return the points, we would get the well-known non-sharing implementation:

fix f = ($) <*> fix $ f = ($) f (fix f) = f (fix f)

I believe, CSE could not be done by GHC in this case as fix f hinges on f's laziness in its first argument. Indeed, as Daniel Wagner suggested, take a look at the following snippet:

let ones = fix (1:) in (ones !! 10000, ones !! 0)

With fix f = let x = f x in x it used up ~54,5 kB, whereas with fix = ($) <*> fix - ~615 kB.

Is it possible to write a sharing point-free fix down somehow?

Zhiltsoff Igor
  • 1,696
  • 5
  • 21
  • What do you mean, "fix could be non-sharing[...]"? That it's possible to implement fix without sharing? Or that the implementation currently in the standard library might not use sharing, because that's an optimization that depends on CSE? The former is clearly true, but I don't see any common subexpressions to eliminate in the standard implementation: it's forced to share. – amalloy Jul 06 '20 at 10:00
  • @amalloy I reckon `Data.Function.fix` to be sharing as it sets up one recursive thunk. But the snippets I wrote down do not match the standard library implementation. They create a new thunk every time. I believe that CSE could not be done by the GHC in my case. I wonder how could we trigger sharing when we cannot set up one entity right away (as it is done in `Data.Function`). Or am I confusing something (I might be, as I am no expert on GHC)? – Zhiltsoff Igor Jul 06 '20 at 10:23
  • Checking the runtime of `fix fact 10000` is not a good test of sharing. Try checking the memory usage of `let ones = fix (1:) in (ones !! 10000, ones !! 0)` instead. – Daniel Wagner Jul 06 '20 at 12:15
  • @DanielWagner right, I agree. I've edited the post. – Zhiltsoff Igor Jul 06 '20 at 12:24

1 Answers1

1

No. let is GHC's cue to share things, and let requires a point.

Daniel Wagner
  • 128,625
  • 9
  • 198
  • 347
  • 1
    It's not that obvious, IMO. For instance, for non-recursive lets, `(\x -> x+x) e` will also share `e`, and the lambda can be made point-free. It looks like with recursion this trick can not be exploited, but we do have some sharing, so I can't completely rule out there is no way to have `fix`. Of course, proving a negative is always hard... – chi Jul 06 '20 at 12:23
  • @chi, so, basically, one of the other possible ways to share is to use `Control.Monad.join`, right? – Zhiltsoff Igor Jul 06 '20 at 12:40
  • 1
    @ZhiltsoffIgor It looks so. Also, `(\x->x+x) = (+) id`. – chi Jul 06 '20 at 12:44
  • @chi Well, the duplication of the third argument of the `S`-combinator is something I used in `fix = ($) fix` - `join` looks a bit more promising, truth be told. Anyway, `join (+)` and `(+) id` are nice ways to write down `(2*)`, indeed :). – Zhiltsoff Igor Jul 06 '20 at 12:48
  • 3
    @ZhiltsoffIgor with functions, `(=<)`, so Monad and Applicative are the same. – Will Ness Jul 06 '20 at 14:24