Skip to content

List fusion: Make traverse et al. good producers

Today, any effectful function that produces a list will not fuse with downstream consumers. So

foo :: Maybe [Int]
foo = fmap (fmap (+1)) . traverse id . fmap Just . fmap (*2) $ [0..100]

will materialise two lists: One for the result of traverse and one for foo.

I think we could get rid of the first list by defining a new build variant, buildA:

buildA :: forall f a. Applicative f => (forall b. (f a -> b -> b) -> b -> b) -> f [a]
buildA g = g (liftA2 (:)) (pure [])
{-# INLINE[1] buildM #-}

"foldr/buildA" forall (c :: a -> b -> b) (z :: b) (g :: forall b. (f a -> b -> b) -> b -> b). GHC.Base.foldr c z `fmap` (buildA g) = g (liftA2 c) (pure z)

traverse2 :: Applicative f => (a -> f b) -> [a] -> f [b]
traverse2 f as = buildA $ \fc fz -> GHC.Base.foldr (\a as -> fc (f a) as) fz as
{-# INLINE traverse2 #-}

Where traverse2 is basically like map, but built with buildA instead of build. I believe we could even reuse the fusion helper mapFB.

Unfortunately, the new RULE will not fire for the example above, for reasons that are beyond my understanding. There's a warning that an existing rule for foldr shadows it, but shouldn't that also apply for foldr/build? Maybe (probably) fmap is inlined too early? Not sure.

Edited by Sebastian Graf
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information