f x = reverse (sort x)
Becomes: f = reverse . sort
The compiler reaches the same conclusion either way.Perhaps you are thinking of RULES pragmas? Which honestly you wouldn't usually reach for unless you really need to force the compiler to optimize in a particular way that is non-trivial.
If a function has type declaration `f :: A -> B -> C` you would expect that its body only gets evaluated when two arguments are actually supplied. So evaluating the function itself using `seq` doesn't do anything. Nor does partial application of a single argument.
To be absolutely clear, the expressions `seq f (putStrLn "hello")` and `seq (f undefined) (putStrLn "hello")` should both not evaluate anything and cause the string hello to be printed rather than raising an exception.
However you can make it otherwise. That breaks the user's intuition about evaluation. Most of the time it's not a problem at all; I just personally don't like it and prefer to be more explicit.
This often occurs in cases where the above hypothetical function `f` requires an intermediate value that's expensive to compute but only relies on the first of its two arguments. Then do you evaluate that expensive intermediate with a partial application of a single argument, or do you not? Neither is satisfactory and so this problem is best avoided.
In your trivial example `reverse . sort` the compiler will inline the dot operator and make it `\x -> reverse (sort x)` and I have no issue there.
That’s not correct, regardless of η expansion. The core of the issue is that this function really takes one argument, regardless of how in practice it is used as if it had two.
> the expressions `seq f (putStrLn "hello")` and `seq (f undefined) (putStrLn "hello")` should both not evaluate anything and cause the string hello to be printed rather than raising an exception.
That’s not how `seq` works, as per language report. The definition is very simple, and does not take into account any kinds of types (such as treating functions differently). The report demands that `seq ⊥ x = ⊥` [1], so if the first argument is ⊥, you get a crash. Whether `x` is also evaluated in this context is up to the compiler actually, but what certainly cannot happen is that a "hello" is printed, because that would require a non-⊥ value to be passed to the surrounding IO context, but the whole `seq` is ⊥, so that’s not possible.
In GHC, seq is essentially a tag that helps the strictness analyzer, and hence the optimizer.
There is one subtlety in GHC that makes it not respect η expansion, and that is strictness properties, namely that `seq ⊥ () = ⊥`, but `seq (\x -> ⊥ x) () = ()`, were you referring to that? (In my opinion this is a violation of the Haskell Report.)
> This often occurs in cases where the above hypothetical function `f` requires an intermediate value that's expensive to compute but only relies on the first of its two arguments.
Lookup tables work that way, yes! I’m using this technique for walking a certain distance on a Bezier curve for example (Code at [2], result picture at [3]) Call that academic and I’ll show you the DIN A1 sized CNC machine I control using that logic ;-P
[1]: https://www.haskell.org/onlinereport/haskell2010/haskellch6.... [2]: https://github.com/quchen/generative-art/blob/master/src/Geo... [3]: https://quchen.github.io/generative-art/generative-art-0.1.0...
My point about seq is really about arity. If a function has arity 2 then partial application of ⊥ cannot cause any part of the function to be executed and therefore the function cannot inspect its argument and find that it is ⊥.
This does actually work here, because the array is created lazily.
> This often occurs in cases where the above hypothetical function `f` requires an intermediate value that's expensive to compute but only relies on the first of its two arguments. Then do you evaluate that expensive intermediate with a partial application of a single argument, or do you not? Neither is satisfactory and so this problem is best avoided.
Computing the intermediate value with a partial application does not actually take away laziness, it only adds sharing. (At least it can be done this way.)
It affects inlining. GHC only inlines fully applied functions, where "fully applied" means applied to as many arguments as it was declared with. So in something like `g = fmap f` your first definition of `f` would never get inlined into g whereas your second definition might.