【问题标题】:Why does Haskell contain so many equivalent functions为什么 Haskell 包含这么多等价函数
【发布时间】:2023-04-05 09:15:01
【问题描述】:

似乎有很多函数做同样的事情,尤其是与 Monads、Functors 和 Applicatives 相关的函数。

示例(从最通用到最不通用):

fmap == liftA == liftM
(<*>) == ap
liftA[2345] == liftM[2345]
pure == return
(*>) == (>>)

不直接基于 FAM 类树的示例:

fmap == map

(我认为 List、Foldable、Traversable 有更多,但看起来大多数前一段时间都变得更加通用,因为我只在旧堆栈溢出/留言板中看到旧的、不太通用的类型签名问题)

我个人觉得这很烦人,因为这意味着如果我需要执行 x,并且某些函数(例如 liftM)允许我执行 x,那么我将使我的函数变得不那么通用,我会只有通过彻底推理类型之间的差异(例如 FAM,或者可能还有 List、Foldable、Traversable 组合)才能注意到这种事情,这根本不适合初学者,因为简单地使用这些类型不是这么难,推理它们的性质和规律需要更多的脑力劳动。

我猜很多这些等价物来自 Applicative Monad 提案。如果这就是它们的原因(而不是我因为缺少可用于混淆的通用功能较少而遗漏的其他原因),它们是否会被弃用/删除?由于破坏了现有代码,我可以理解等待很长时间才能删除它们,但肯定弃用是个好主意吗?

【问题讨论】:

  • 始终使用fmappure(*&gt;)
  • @ZhekaKozlov 当然可以,但是其他人编写的代码我可能必须与之交互,而且我可能不知道的情况不仅仅是fmap pure*&gt;全部。

标签: haskell


【解决方案1】:

简短的回答是“历史”和“规律性”。

最初“地图”是为列表定义的。然后引入了类型类,以及 Functor 类型类,因此任何函子的“映射”的通用版本必须被称为不同的东西,否则现有代码将被破坏。因此是“fmap”。

然后单子出现了。 monad 的实例不需要是函子,因此创建了“liftM”,以及“liftM2”、“liftM3”等。当然,如果一个类型是 Monad 和 Functor 的实例,那么 fmap = liftM。

Monad 也有“ap”,用于f `ap` arg1 `ap` arg2 等表达式。这非常方便,但随后添加了 Applicative Functors。 () 对应用函子的作用与“ap”相同,但由于许多应用函子不是单子,因此必须将其称为不同的名称。同样,liftAx 与 liftMx 以及“纯”与“返回”。

【讨论】:

  • 在数学上,monad 只是一种特殊类型的函子(它是两个伴随函子的组合)。虽然不必强制为具有 Monad 实例的类型定义 Functor 实例,但始终可以这样做 (fmap f xs == xs &gt;&gt;= return . f)。
  • 有道理,我建议弃用所有不太通用的函数怎么样?
  • 您在fmap 的报道有一些非常轻微的不准确之处。当Functor 出现时,map 立即被推广到所有Functors,而不仅仅是列表。然而,当有人误用map(当人们只想将它用于列表时询问Functors)时,人们对会弹出的类型错误感到不安,所以在那之后,map 被重命名为fmap,而原来的map 被重新引入,只针对列表。
  • @JustinL。我真的希望有类似EasyPrelude 这样的东西。我可以看到它对初学者很有用,但我觉得它有点烦人。
【解决方案2】:

但它们并不等价。 haskell 中的等价物可以互换,在功能上完全没有区别。例如考虑purereturn

编辑:我写了一些例子,但它们真的很糟糕,因为它们涉及Maybe a,一种既是应用程序又是单子的类型,所以这些函数可以互换使用。

虽然有些类型是 applicatives 但不是 monads(请参阅this 问题的示例),通过研究以下表达式的类型,我们可以看到这可能会导致一些障碍:

pure 1 >>= pure :: (Monad m, Num b) => m b

【讨论】:

  • 实际上我的两个例子都不好,pure 1 &gt;&gt;= safedivide 0 再次进行类型检查,因为Maybe 既是应用程序又是单子。
  • 它们是等价的,return 总是可以替换为pure,依此类推。也许我用错了词,但我的意思没有错。我认为我对“(从最通用到最不通用)”的使用清楚地表明我理解它们只能在一个方向上被替换。
  • 另外,当两者都进行类型检查时,它们是等价的。我知道的唯一区别是类型的通用性。
  • 我会说,虽然它有时是可取的,但不应该在标准库中为您完成。它应该是您在需要时做的事情,只需将顶级函数的类型签名调整为您想要的。比如monadicAddTwo :: (Monad m, Num b) =&gt; f b -&gt; fb) 然后monadicAddTwo = fmap (+ 2)。因为无论如何你都应该写出顶级类型签名。
  • @kai 问题是:当pure 完全包含类似return 的东西时,为什么还要存在(因为所有单子都是应用程序)?确实没有技术原因。它的存在是出于历史原因(Monad 类型类是在 FunctorApplicative 之前实现的),而在实现实例时,像 ap 这样的东西有时会很有用:如果你已经写了一个 Monad 实例,你可以说instance Applicative ... where pure = return; (&lt;*&gt;) = ap。至于限制类型签名,限制将朝着相反的方向发展,朝着不那么普遍的方向发展。
【解决方案3】:

我个人觉得这很烦人,因为这意味着如果我需要做 x,而诸如 liftM 之类的函数允许我做 x,那么我会让我的函数变得不那么通用

这个逻辑是倒退的。

通常你会提前知道你想写的东西的类型,是IO String(Foldable f, Monoid t, Monad m) =&gt; f (m t) -&gt; m t 或其他。让我们以第一种情况为例,getLineCapitalized :: IO String。你可以写成

getLineCapitalized = liftM (map toUpper) getLine

getLineCapitalized = fmap (fmap toUpper) getLine

前者是否因为使用了专用函数liftMmap 而“不那么通用”?当然不是。这本质上是一个生成列表的 IO 操作。它不能通过将其更改为第二个版本而变得“更通用”,因为无论如何,那些fmaps 的类型将固定为IO[]。所以,第二个版本没有优势。

通过编写第一个版本,您可以免费向读者提供上下文信息。在liftM (map foo) bar 中,读者知道bar 将是某个返回列表的monad 中的一个动作。在fmap (fmap foo) bar 中,它可以是任何类型的双重嵌套结构。如果bar 是复杂的东西而不仅仅是getLine,那么这类信息有助于更容易理解bar 中发生的事情。

一般来说,你应该分两步写一个函数。

  1. 决定函数的类型。让它像你想要的一般或具体。函数的类型越通用,参数化对它的行为的保证就越强。

  2. 一旦确定了函数的类型,就可以使用最具体的可用函数来实现它。通过这样做,您向函数的读者提供了最多的信息。这样做不会失去任何通用性或参数性保证,因为它们仅取决于您在步骤 1 中已经确定的类型。


针对 cme​​ts 进行编辑:我想起了使用最具体可用功能的最大原因,即捕捉错误length :: [a] -&gt; Int 类型本质上是我仍然使用 GHC 7.8 的全部原因。从来没有发生过我想要获取未知Foldable 结构的长度。另一方面,我绝对不想意外地取一对的长度,或者取 foo bar baz 的长度,我认为它的类型是 [a],但实际上是类型 Maybe [a]

在 Haskell 标准的其余部分尚未涵盖的可折叠用例中,镜头是一种功能更强大的替代品。如果我想要Maybe t 的“长度”,lengthOf _Just :: Maybe t -&gt; Int 清楚地表达了我的意图,编译器可以检查程序是否真的符合我的意图;我可以继续写lengthOf _NothinglengthOf _Left等。显式优于隐式。

【讨论】:

  • 按照这个逻辑你定义lengthList :: [a] -&gt; IntfoldrTraversable :: Traversable t =&gt; (a -&gt; b -&gt; b) -&gt; b -&gt; t a -&gt; b,那(+.+) :: Double -&gt; Double -&gt; Double呢。现在显然你不这样做,因为那会很愚蠢,那么为什么要使用liftM?现在你可能会回答“但那些还不可用”,如果它们是,你会使用它们吗?另外,当liftMliftA&gt;&gt; 可用时,为什么它们不可用?似乎您要么必须为每个组合包含特定功能,要么没有任何不必要的特定功能,要么不一致。
  • 我还认为,使用具有不必要的特定类型的函数获得的“清晰度”增加不值得你必须记住的函数数量增加。在我之前的评论中采取逻辑极端,会有更多的函数(Functor 可以变成 Monad、Applicative、List、Maybe、IO、Either、ZipList 等),所有这些你都想记住.通过简单地没有任何过于具体的功能,您可以节省大量的脑力开销,并且您肯定需要做更多的推理,但这并不难。
  • 我没有定义我自己的类型限制同义词。我只使用 Haskell 程序员已经知道的 Haskell 标准中的那些,因为我正在编写一个 Haskell 程序。当然,任何标准库的确切内容都不可避免地有些武断。从长远来看,学习一些额外的名字,比如map = fmap(.) = (&lt;$&gt;),比一辈子在fmap 的汤里涉水要便宜得多。
  • 标准库的内容并不完全是历史偶然。它们还反映了人类程序员对其程序的看法。函数组合(.) 是一个基本概念,尽管(.) 本身是(&lt;$&gt;)fmap 的类型限制形式。您是否会争辩说也不应该使用(.)
  • 关于捕捉错误的主题 - 使用更通用的函数可以让您为您尝试编写的函数提供更通用的类型签名(即更多参数)。这可以通过类型检查排除大量虚假实现。 Functor f =&gt; (a -&gt; b) -&gt; f a -&gt; f b 类型只有一个有趣的函数,但Monad m =&gt; (a -&gt; b) -&gt; m a -&gt; m b 类型的函数有无限多。
【解决方案4】:

有一些“冗余”函数,如 liftMapliftA,它们有非常实际的用途,如果去掉它们会导致功能丧失 --- 你可以使用 liftM、@987654325 @ 和 liftA 来实现你的 FunctorApplicative 实例,如果你写的只是一个 Monad 实例。它让你偷懒,然后说:

instance Monad Foo where
    return = ...
    (>>=) = ...

现在您已经完成了定义Monad 实例的所有有价值的工作,但这不会编译。为什么?因为您还需要一个FunctorApplicative 实例。

所以,由于您正在快速进行原型设计,或者懒惰,或者想不出更好的方法,您可以免费获得 FunctorApplicative 实例:

instance Functor Foo where
    fmap = liftM

instance Applicative Foo where
    pure  = return
    (<*>) = ap

事实上,当您已经定义了 Monad 实例时,您可以在需要快速定义 FunctorApplicative 实例的任何地方复制并粘贴该代码块。

fmapDefaultData.Traversable 也是如此。如果你实现了Traversable,你也可以实现FoldableFunctor

instance Functor Bar where
    fmap = fmapDefault

不需要额外的工作!

然而,有一些冗余函数实际上没有实际用途,除了在Functor 不是Monad 的超类时的历史事故。这些在现有的方面几乎为零使用/点......并且包括liftM2liftM3 等,以及(&gt;&gt;) 和朋友。

【讨论】:

  • 对于整个“使用冗余函数来实现超类”来说,允许自动派生这些超类不是一个更好的选择。因此,如果您没有手动指定instance Functor Foo where 或其他任何内容,但您有instance Monad Foo where,则让编译器简单地派生fmappure&lt;*&gt;
  • 为了扩展一点,我认为instance Monad Foo where(&gt;&gt;=) = ...pure = ... 应该是使Foo 成为MonadApplicativeFunctor.
猜你喜欢
  • 2018-11-30
  • 1970-01-01
  • 2016-09-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-24
  • 2011-10-31
相关资源
最近更新 更多