有没有一种有效,轻松的方法来将foldMap与遍历融合在一起?

时间:2019-05-08 17:33:26

标签: haskell traversal fold

关于Haskell图书馆邮件列表的最新建议使我考虑以下内容:

ft :: (Applicative f, Monoid m, Traversable t)
   -> (b -> m) -> (a -> f b) -> t a -> f m
ft f g xs = foldMap f <$> traverse g xs

我注意到Traversable约束可以减弱为Foldable

import Data.Monoid (Ap (..))  -- Requires a recent base version

ft :: (Applicative f, Monoid m, Foldable t)
   -> (b -> m) -> (a -> f b) -> t a -> f m
ft f g = getAp . foldMap (Ap . fmap f . g)

在原始提案中,f应该是id,导致

foldMapA
  :: (Applicative f, Monoid m, Foldable t)
   -> (a -> f m) -> t a -> f m
--foldMapA g = getAp . foldMap (Ap . fmap id . g)
foldMapA g = getAp . foldMap (Ap . g)

这绝对比“遍历-折叠”方法好。

但是在更一般的ft中,存在一个潜在的问题:fmapf函子中可能很昂贵,在这种情况下,融合版本可能比原始版本更昂贵。 !

用于处理昂贵的fmap的常用工具是YonedaCoyoneda。由于我们需要多次举起而只需要降低一次,因此Coyoneda可以为我们提供帮助:

import Data.Functor.Coyoneda

ft' :: (Applicative f, Monoid m, Foldable t)
    => (b -> m) -> (a -> f b) -> t a -> f m
ft' f g = lowerCoyoneda . getAp
  . foldMap (Ap . fmap f . liftCoyoneda . g)

因此,现在我们将所有昂贵的fmap替换为一个(埋在lowerCoyoneda中)。问题解决了?不完全是。

Coyoneda的问题在于它的liftA2 strict 。所以如果我们写类似

import Data.Monoid (First (..))

ft' (First . Just) Identity $ 1 : undefined
-- or, importing Data.Functor.Reverse,
ft' (Last . Just) Identity (Reverse $ 1 : undefined)

然后它将失败,而ft对此毫无问题。有办法吃蛋糕吗?也就是说,仅使用Foldable约束的版本是fmap函子中traverse的O(1)的O(1)倍,是{{1} f


注意:我们可以用ftliftA2Coyoneda 有点懒惰:

liftA2 f m n = liftCoyoneda $
  case (m, n) of
    (Coyoneda g x, Coyoneda h y) -> liftA2 (\p q -> f (g p) (h q)) x y

这足以使它产生对ft' (First . Just) Identity $ 1 : 2 : undefined的回答,但对ft' (First . Just) Identity $ 1 : undefined却没有回答。我看不出有什么比这更懒的明显方法,因为对存在物进行模式匹配必须始终严格。

1 个答案:

答案 0 :(得分:1)

我不认为有可能。避免在元素上使用fmap似乎需要对容器的结构有所了解。例如,可以编写列表的Traversable实例

traverse f (x : xs) = liftA2 (:) (f x) (traverse f xs)

我们知道(:)的第一个参数是单个元素,因此我们可以使用liftA2将对该元素的操作进行映射的过程与对该元素的结果进行合并的过程结合起来操作,并将结果与​​列表的其余部分相关联。

在更一般的上下文中,可以使用带有伪造Monoid实例的岩浆类型来忠实地捕获褶皱的结构:

data Magma a = Bin (Magma a) (Magma a) | Leaf a | Nil
  deriving (Functor, Foldable, Traversable)

instance Semigroup (Magma a) where
  (<>) = Bin
instance Monoid (Magma a) where
  mempty = Nil

toMagma :: Foldable t => t a -> Magma a
toMagma = foldMap Leaf

我们可以写

ft'' :: (Applicative f, Monoid m, Foldable t)
   => (b -> m) -> (a -> f b) -> t a -> f m
ft'' f g = fmap (lowerMagma f) . traverse g . toMagma

lowerMagma :: Monoid m => (a -> m) -> Magma a -> m
lowerMagma f (Bin x y) = lowerMagma f x <> lowerMagma f y
lowerMagma f (Leaf x) = f x
lowerMagma _ Nil = mempty

但是Traversable实例中有麻烦:

traverse f (Leaf x) = Leaf <$> f x

这正是我们要避免的麻烦。而且没有懒惰的解决方案。如果遇到Bin l r,我们将懒惰地确定lr是树叶。所以我们被困住了。如果我们对Traversable施加ft''约束,则可以捕获具有更丰富的岩浆类型(例如lens中使用的岩浆类型)的遍历结果,我怀疑可以让我们做些更聪明的事情,尽管我还没有发现任何东西。