2

Say we have a (contrived) function like so:

import Data.List (sort)

contrived :: Ord a => [a] -> [a] -> [a]
contrived a b = (sort a) ++ b

And we partially apply it to use elsewhere, eg:

map (contrived [3,2,1]) [[4],[5],[6]]

On the surface, this works as one would expect:

[[1,2,3,4],[1,2,3,5],[1,2,3,6]]

However, if we throw some traces in:

import Debug.Trace (trace)

contrived :: Ord a => [a] -> [a] -> [a]
contrived a b = (trace "sorted" $ sort a) ++ b
map (contrived $ trace "a value" [3,2,1]) [[4],[5],[6]]

We see that the first list passed into contrived is evaluated only once, but it is sorted for each item in [4,5,6]:

[sorted
a value
[1,2,3,4],sorted
[1,2,3,5],sorted
[1,2,3,6]]

Now, contrived can be rather simply translated to point-free style:

contrived :: Ord a => [a] -> [a] -> [a]
contrived a = (++) (sort a)

Which when partially applied:

map (contrived [3,2,1]) [4,5,6]

Still works as we expect:

[[1,2,3,4],[1,2,3,5],[1,2,3,6]]

But if we again add traces:

contrived :: Ord a => [a] -> [a] -> [a]
contrived a = (++) (trace "sorted" $ sort a)
map (contrived $ trace "a value" [3,2,1]) [[4],[5],[6]]

We see that now the first list passed into contrived is evaluated and sorted only once:

[sorted
a value
[1,2,3,4],[1,2,3,5],[1,2,3,6]]

Why is this so? Since the translation into pointfree style is so trivial, why can't GHC deduce that it only needs to sort a once in the first version of contrived?


Note: I know that for this rather trivial example, it's probably preferable to use pointfree style. This is a contrived example that I've simplified quite a bit. The real function that I'm having the issue with is less clear (in my opinion) when expressed in pointfree style:

realFunction a b = conditionOne && conditionTwo
  where conditionOne = map (something a) b
        conditionTwo = somethingElse a b

In pointfree style, this requires writing an ugly wrapper (both) around (&&):

realFunction a = both conditionOne conditionTwo
  where conditionOne = map (something a)
        conditionTwo = somethingElse a
        both f g x = (f x) && (g x)

As an aside, I'm also not sure why the both wrapper works; the pointfree style of realFunction behaves like the pointfree style version of contrived in that the partial application is only evaluated once (ie. if something sorted a it would only do so once). It appears that since both is not pointfree, Haskell should have the same issue that it had with the non-pointfree contrived.

Bailey Parker
  • 15,599
  • 5
  • 53
  • 91

1 Answers1

3

If I understand correctly, you are looking for this:

contrived :: Ord a => [a] -> [a] -> [a]
contrived a = let a' = sort a in \b -> a' ++ b
                    -- or ... in (a' ++)

If you want the sort to be computed only once, it has to be done before the \b.

You are correct in that a compiler could optimize this. This is known as the "full laziness" optimization.

If I remember correctly, GHC does not always do it because it's not always an actual optimization, in the general case. Consider the contrived example

foo :: Int -> Int -> Int
foo x y = let a = [1..x] in length a + y

When passing both arguments, the above code works in constant space: the list elements are immediately garbage collected as they are produced.

When partially applying x, the closure for foo x only requires O(1) memory, since the list is not yet generated. Code like

let f = foo 1000 in f 10 + f 20  -- (*)

still run in constant space.

Instead, if we wrote

foo :: Int -> Int -> Int
foo x = let a = [1..x] in (length a +)

then (*) would no longer run in constant space. The first call f 10 would allocate a 1000-long list, and keep it in memory for the second call f 20.


Note that your partial application

... = (++) (sort a)

essentially means

... = let a' = sort a in \b -> a' ++ b

since argument passing involves a binding, as in let. So, the result of your sort a is kept around for all the future calls.

chi
  • 111,837
  • 3
  • 133
  • 218
  • +1 (+ more if I could!) Thanks for a really clear explanation. I see why GHC doesn't make this optimization by default given the general case. I forgot to mention that I tried using `($!)` to force strict evaluation of the sort. I must've been doing it wrong, because lo and behold `($!)` is just another spelling of the let construct you suggested! – Bailey Parker Jan 21 '17 at 09:02
  • @BaileyParker Actually, `let` does not force evaluation immediately, while `$!` does force the argument. `let x = e in f x + g x` will evaluate `e` only when it is needed by `f,g`, but will save its value in memory for the second call. – chi Jan 21 '17 at 09:53