【问题标题】:Understanding `mapA` to Gain Intuition理解`mapA`以获得直觉
【发布时间】:2015-01-02 02:29:21
【问题描述】:

我正在 Brent Yorgey 的 2013 年 UPenn lecture 中进行练习以实施 mapA

mapA :: Applicative f => (a -> f b) -> ([a] -> f [b])

我正在尝试获得对这个函数的直觉。这个功能有什么用?我不是在质疑它的实用性——只是想理解它。

此外,我正在寻找从 a -> f b[a] -> f [b] 的提示。

【问题讨论】:

  • 您同意mapM 有用吗?我认为很明显,“对集合的每个元素执行此操作”是一个无处不在的概念。这就是mapM/AmapAmapM 用于应用程序。由于并非每个 Applicative 都是 Monad,因此您有时可能需要一个“形状”为 mapM 的函数,它只需要 Applicative 约束。因此,mapA.
  • mapA f = dist . map f,其中dist 来自您的question
  • 你也可以在Data.Traversable的列表实例中找到这个函数的实现
  • The essence of the iterator pattern 描述成语的几种用途,使用更通用的traverse 函数。

标签: haskell


【解决方案1】:

如果我们对f 一无所知,那么像a -> f b 这样的函数会让我们将东西放入f-boxes,但我们会完全陷入困境。您可能熟悉Functor。如果我们知道fFunctor,那么我们就可以转换f 内部的东西,但我们仍然基本上被卡住了---f 形成了一堵我们无法跨越的不动墙。

我们为什么要关心?好吧,当我们尝试构造函数[a] -> f [b] 时,我们需要了解如何对as 的集合进行操作。如果我们喜欢(并且它存在),也许我们可以直接关闭第一个并通过a -> f b 提供它,然后将结果包装在一个列表中:

unsatisfying :: Functor f => (a -> f b) -> ([a] -> f [b])
unsatisfying inject (a : _) = fmap (\x -> [x]) (inject a)

但我们不仅在[a] 上有一个不完整的模式匹配,而且我们显然违反了这个函数的精神——我们更愿意使用所有as。不幸的是,只知道f 甚至知道fFunctor 只能让我们了解

stillUnsatisfying :: Functor f => (a -> f b) -> ([a] -> [f b])
stillUnsatisfying inject as = map inject as

问题在于,仅仅因为我们拥有f-containers 的集合并不意味着我们可以找到任何方法来集体对待它们。我们想以某种方式将我们的集合[f b]“粘合”在一起。如果我们能做到这一点,那么像[a] -> f [b] 这样的函数听起来就像“将我们的列表[a] 分解成碎片,使用inject 将它们单独传递到f,然后将所有(f b)s 聚集在一起,然后重新组装里面的列表”。

显然,我们需要一种将Functors 组合在一起的方法,以及一种对f 的“内部”独立部分进行操作的方法。

这就是Applicative 的用武之地。不过,我不会准确介绍它。相反,让我们看一个等效的类型类

class Functor f => Monoidal f where
  basic :: a -> f a
  glom  :: f a -> f b -> f (a, b)

证明MonoidalApplicative 是等价的,这是一个有趣的练习,但是您马上可以看到glom 提供了我们正在寻找的东西。此外,basic/pure 使我们能够根据需要将列表的原始片段注入到 f 中(例如,如果我们的 [a] 为空,那么我们需要将一个空列表注入到f 不使用 a -> f b,因为我们不能——这看起来像 basic [] :: f [b])。

所以Applicative 让您不仅可以在函子内部进行转换,还可以将一堆函子聚集在一起,并对函子内部的所有部分进行操作。

【讨论】:

  • "但是你可以立即看到 glom 提供了我们正在寻找的东西"。我不能。
  • 通常我们会使用(<*>) :: f (a -> b) -> f a -> f b,但我们可以使用glom 作为f <*> a = fmap ($) (glom f a)。试试看!
  • 但是为什么 Monoidal 和 Applicative 的等价性与原始问题(即如何实现 mapA)有关?
  • 因为Monoidal 更清楚地表明Applicative 的核心是使用标准产品将函子粉碎在一起的能力。这被Applicative 公式所掩盖。
【解决方案2】:

您已经完成了一半,但您要寻找的最后一个函数是 LYAH 中定义的 sequenceA 函数。这是我的实现:

sequenceA :: Applicative f => [f a] -> f [a]
sequenceA = foldr ((<*>) . fmap (:)) (pure [])

从那里mapA 函数很简单。只需添加另一个参数并组合两个函数:

mapA :: Applicative f => (a -> f b) -> [a] -> f [b]
mapA f xs = foldr ((<*>) . fmap (:) . f) (pure []) xs
-- Or, you could implement it this more elegant, albeit slightly slower, way:
mapA = (sequenceA .) . map

你有一个实现供你查看。至于实用性,它在 IO 中特别有用,当你有一个像 ["Foo","Bar","Baz"] 这样的列表,并且你想 putStrLn 所有这些功能时。这样做,您将需要映射每个值和sequence 它,mapA 就是这样做的。请注意,这些函数具有 monadic 等价物,但以纯粹的 Applicative 方式实现它们是一个很好的练习。

在处理Applicatives 列表时,这些函数都非常有用,允许人们更轻松地操作其中的值,而无需使用半吨 fmaps。

【讨论】:

    【解决方案3】:

    我们已经知道map :: (a -&gt; b) -&gt; ([a] -&gt; [b]) 很有用。它将一个函数应用于列表的每个元素。

    对于应用程序fa -&gt; f b 的松散但有启发性的解释是,它是一个接受a、执行应用程序“动作”并返回b 的函数。例如,如果fIO,那么“动作”可能是从磁盘读取。 mapA :: (a -&gt; f b) -&gt; ([a] -&gt; f [b]) 可以解释为将此“函数”应用于列表的每个元素。

    【讨论】:

      猜你喜欢
      • 2022-10-13
      • 2018-10-27
      • 2015-10-08
      • 2021-09-28
      • 2015-11-26
      • 1970-01-01
      • 1970-01-01
      • 2020-02-05
      • 2021-12-22
      相关资源
      最近更新 更多