Search code examples
haskelllazy-evaluationstrictness

Understanding non-strictness in Haskell with a recursive example


What is the difference between this two, in terms of evaluation?

Why this "obeys" (how to say?) non-strictness

recFilter :: (a -> Bool) -> [a] -> [a]
recFilter _ [] = []
recFilter p (h:tl) = if (p h) 
  then h : recFilter p tl
  else recFilter p tl

while this doesn't?

recFilter :: (a -> Bool) -> [a] -> Int -> [a]
recFilter _ xs 0 = xs
recFilter p (h:tl) len
  | p(h)  = recFilter p (tl ++ [h]) (len-1)
  | otherwise = recFilter p tl (len-1)

Is it possible to write a tail-recursive function non-strictly?

To be honest I also don't understand the call stack of the first example, because I can't see where that h: goes. Is there a way to see this in ghci?


Solution

  • The second implementation does not make much sense: a variable named len will not contain the length of the list. You thus need to pass this, for infinite lists, this would not work, since there is no length at all.

    You likely want to produce something like:

    recFilter :: (a -> Bool) -> [a] -> [a]
    recFilter p = go []
        where go ys [] = ys  -- (1)
              go ys (x:xs) | p x = go (ys ++ [x]) xs
                           | otherwise = go ys xs
    

    where we thus have an accumulator to which we append the items in the list, and then eventually return the accumulator.

    The problem with the second approach is that as long as the accumulator is not returned, Haskell will need to keep recursing until at least we reach weak head normal form (WHNF). This means that if we pattern match the result with [] or (_:_), we will need at least have to recurse until case one, since the other cases only produce a new expression, and it will thus not yield a data constructor on which we can pattern match.

    This in contrast to the first filter where if we pattern match on [] or (_:_) it is sufficient to stop at the first case (1), or the third case 93) where the expression produces an object with a list data constructor. Only if we require extra elements to pattern match, for example (_:_:_), it will require to evaluate the recFilter p tl in case (2) of the first implementation:

    recFilter :: (a -> Bool) -> [a] -> [a]
    recFilter _ [] = []  -- (1)
    recFilter p (h:tl) = if (p h) 
      then h : recFilter p tl  -- (2)
      else recFilter p tl
    

    For more information, see the Laziness section of the Wikibook on Haskell that describes how laziness works with thunks.