There is a technique I've seen a few times with foldr
. It involves using a function in place of the accumulator in a foldr
. I'm wondering when it is necessary to do this, as opposed to using an accumulator that is just a regular value.
Most people have seen this technique before when using foldr
to define foldl
:
myFoldl :: forall a b. (b -> a -> b) -> b -> [a] -> b
myFoldl accum nil as = foldr f id as nil
where
f :: a -> (b -> b) -> b -> b
f a continuation b = continuation $ accum b a
Here, the type of the combining function f
is not just a -> b -> b
like normal, but a -> (b -> b) -> b -> b
. It takes not only an a
and b
, but a continuation (b -> b
) that we need to pass the b
to in order to get the final b
.
I most recently saw an example of using this trick in the book Parallel and Concurrent Programming in Haskell. Here is a link to the source code of the example using this trick. Here is a link to the chapter of the book explaining this example.
I've taken the liberty of simplifying the source code into a similar (but shorter) example. Below is a function that takes a list of Strings, prints out whether each string's length is greater than five, then prints the full list of only the Strings that have a length greater than five:
import Text.Printf
stringsOver5 :: [String] -> IO ()
stringsOver5 strings = foldr f (print . reverse) strings []
where
f :: String -> ([String] -> IO ()) -> [String] -> IO ()
f str continuation strs = do
let isGreaterThan5 = length str > 5
printf "Working on \"%s\", greater than 5? %s\n" str (show isGreaterThan5)
if isGreaterThan5
then continuation $ str : strs
else continuation strs
Here's an example of using it from GHCi:
> stringsOver5 ["subdirectory", "bye", "cat", "function"]
Working on "subdirectory", greater than 5? True
Working on "bye", greater than 5? False
Working on "cat", greater than 5? False
Working on "function", greater than 5? True
["subdirectory","function"]
Just like in the myFoldl
example, you can see that the combining function f
is using the same trick.
However, it occurred to me that this stringsOver5
function could probably be written without this trick:
stringsOver5PlainFoldr :: [String] -> IO ()
stringsOver5PlainFoldr strings = foldr f (pure []) strings >>= print
where
f :: String -> IO [String] -> IO [String]
f str ioStrs = do
let isGreaterThan5 = length str > 5
printf "Working on \"%s\", greater than 5? %s\n" str (show isGreaterThan5)
if isGreaterThan5
then fmap (str :) ioStrs
else ioStrs
(Although maybe you could make the argument that IO [String]
is a continuation?)
I have two questions regarding this:
- Is it every absolutely necessary to use this trick of passing a continuation to
foldr
, instead of usingfoldr
with a normal value as an accumulator? Is there an example of a function that absolutely can't be written usingfoldr
with a normal value? (Aside fromfoldl
and functions like that, of course.) - When would I want to use this trick in my own code? Is there any example of a function that can be significantly simplified by using this trick?
- Is there any sort of performance considerations to take into account when using this trick? (Or, well, when not using this trick?)