1

If I for example apply map (just an example, could be filter or something else as well) on the same list 3 times, is Haskell going to pass through the list three times or only once? Example to run in ghci:

map (+1) $ map (*2) $ map (^2) [1..100]

What I'm wondering is if the complexity is 3n where n is the size of the list as it would be in an imperative language, or if the laziness in Haskell optimizes it to 1n by only going through the list once, and doing the three operations on each element at the same time. So with 1 it would

  1. raise it to 2
  2. multiply it by 2
  3. add 1

and then move on to the next element, instead of first going to through the whole list while raising every element to 2, then going through the list again and multiplying every element by and then go through the list a third time to increment every element by one.

So which is, it? Is Haskell going through the list 3 times or only once?

Micha Wiedenmann
  • 19,979
  • 21
  • 92
  • 137
lapurita
  • 945
  • 1
  • 5
  • 11
  • 3
    The answer is "yes". At least from a theoretical viewpoint, there's no difference here--either way, you're doing 3N operations (though laziness means N = number of results used rather than number of inputs). Caching can create a difference, but depending on the complexity of the code to execute vs. the amount of data involved, that could go either way. – Jerry Coffin Sep 15 '21 at 21:36
  • 3
    If you're talking about time complexity, `O(n)` = `O(3n)`. If you're talking about execution performance, notice that the "list" is a lazy value as well, and items are generated as you go anyway, so this really isn't comparable to any "imperative" list. As for your actual question: ghci implements [short cut fusion](https://wiki.haskell.org/Short_cut_fusion), also explained [here](https://stackoverflow.com/q/38905369/1048572) – Bergi Sep 15 '21 at 21:44
  • 4
    When someone demands the first element of the resulting list, only the first elements will be demanded from the first map, which will demand only the first element from the second, and so on. Try this to convince yourself: `head $ map (+1) $ map (*2) $ map (^2) [1,error "crash!"]`: no error will be observed. – chi Sep 15 '21 at 21:44

1 Answers1

6

You can do a simple check under ghci: set statistics mode on, and try running the expression both ways but with a significant size. Sum the list and print its sum to force full evaluation.

Like this:

$ ghci
 GHCi, version 8.8.4: https://www.haskell.org/ghc/  :? for help
 λ> 
 λ> :set +s
 λ> 
 λ> n = 1000000
 (0.00 secs, 24,296 bytes)
 λ> 
 λ> sum ( map (+1) $ map (*2) $ map (^2) [1..n] )
 666667666668000000
 (1.36 secs, 865,692,136 bytes)
 λ> 
 λ> sum ( map  ((+1) . (*2) . (^2))  [1..n] )
 666667666668000000
 (1.03 secs, 753,692,056 bytes)
 λ> 

So there is some performance gain in using the optimized expression, but it is much less than 3X.

Addendum:

Of course, for the sake of completeness, we also have to check with GHC code compiled with -O2, using this syntax on the command line to get the statistics:

$ myHaskellExe +RTS -s -RTS
...

The performance is much better than with ghci, so let's time for 100 millions rather than just one million.

Source code #1:

sumx1 :: Integer -> Integer
sumx1 n = sum ( map (+1) $ map (*2) $ map (^2) [1..n] )

main:: IO ()
main = do
    let  k   = 100*1000*1000
         res = sumx1 k
    putStrLn $ "k=" ++ (show k) ++ "  res1=" ++ (show res)

Result #1:

k=100000000  res1=666666676666666800000000
  12,679,831,656 bytes allocated in the heap
...
  GC      time    0.029s  (  0.028s elapsed)
  Total   time    5.457s  (  5.467s elapsed)

Source code #2:

sumx2 :: Integer -> Integer
sumx2 n = sum ( map  ((+1) . (*2) . (^2))  [1..n] )

main:: IO ()
main = do
    let  k   = 100*1000*1000
         res = sumx2 k
    putStrLn $ "k=" ++ (show k) ++ "  res2=" ++ (show res)

Result #2:

k=100000000  res2=666666676666666800000000
  12,679,831,656 bytes allocated in the heap
...
  GC      time    0.030s  (  0.029s elapsed)
  Total   time    5.500s  (  5.512s elapsed)

Hence, with GHC the performance difference between the two expressions is insignificant. High-level optimizer at work probably.

jpmarinier
  • 4,427
  • 1
  • 10
  • 23
  • 4
    It's not really "for the sake of completeness". Performance tests in ghci mean practically nothing. GHC may create quite different code when compiling. It doesn't try hard at all to generate efficient code in ghci. – amalloy Sep 16 '21 at 00:00
  • @amalloy Yes that's right, so I have added the timings for GHC optimized code. This time there is no significant performance difference between the two expressions. – jpmarinier Sep 17 '21 at 23:01