为什么Haskell包含这么多等价函数

时间:2016-04-11 17:58:04

标签: haskell

似乎有很多功能可以做同样的事情,特别是与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组合)来注意那种事情,这对于初学者来说并不友好,因为只是简单地使用这些类型而不是#39所有这些都很难,对其属性和法律的推理需要更多的心理努力。

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

4 个答案:

答案 0 :(得分:5)

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

最初为地图定义了“地图”。然后引入类型类,使用Functor类型类,因此任何仿函数的“map”的通用版本必须被称为不同的东西,否则现有代码将被破坏。因此“fmap”。

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

Monads也有“ap”,用于像f `ap` arg1 `ap` arg2这样的表达式。这非常方便,但随后添加了Applicative Functors。 (&lt; *&gt;)和'ap'一样对应用函子执行相同的工作,但是因为许多应用函子不是monad所以它必须被称为不同的东西。同样,liftAx与liftMx相比,“pure”与“return”相同。

答案 1 :(得分:1)

但它们并不相同。 haskell中的等价物可以互换,功能上没有任何区别。例如,请考虑purereturn

编辑:我写了一些例子,但是他们真的很糟糕,因为他们涉及Maybe a,这是一个既适用又适合monad的类型,所以函数可以互换使用。

虽然有些类型是应用但不是monad(请参阅this问题示例),通过研究以下表达式的类型,我们可以看到这可能导致一些道路颠簸:

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

答案 2 :(得分:0)

  

我个人认为这很烦人,因为这意味着如果我需要做x,而某些函数如liftM允许我做x,那么我将使我的函数不那么通用

这种逻辑是倒退的。

通常,您事先知道要写的东西的类型,无论是IO String还是(Foldable f, Monoid t, Monad m) => f (m t) -> m t还是其他什么。我们来看第一个案例getLineCapitalized :: IO String。你可以把它写成

getLineCapitalized = liftM (map toUpper) getLine

getLineCapitalized = fmap (fmap toUpper) getLine

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

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

通常,您应该分两步编写一个函数。

  1. 确定函数的类型应该是什么。根据需要将其设为一般或特定。函数的类型越一般,就越能保证你从参数化中获得它的行为。

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

  3. 编辑以回应评论:我被提醒使用最具体的功能的最大原因,即捕获错误。类型length :: [a] -> Int基本上是我仍然使用GHC 7.8的全部原因。我从未想过采用未知Foldable结构的长度。另一方面,我绝对不希望意外地占用一对的长度,或者取foo bar baz的长度,我认为其类型为[a],但实际上有Maybe [a]类型

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

答案 3 :(得分:0)

有一些“冗余”功能,例如liftMapliftA,它们具有非常实际的用途并将其取出会导致功能丧失 - 您可以使用liftMapliftA实施FunctorApplicative个实例,如果你所写的只是Monad个实例。它让你变得懒惰,并说:

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

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

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

instance Functor Foo where
    fmap = liftM

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

实际上,只要您已经定义了Functor实例,就可以在需要的任何地方复制并粘贴该块代码,以快速定义ApplicativeMonad实例。

来自fmapDefault的{​​{1}}同样如此。如果您已实施Data.Traversable,则还可以实施TraversableFoldable

Functor

无需额外工作!

然而,有一些冗余函数除了instance Functor Bar where fmap = fmapDefault 不是Functor的超类之外的历史事故时,实际上没有实际用法。这些在现有内容中实际上没有使用/点数......包括MonadliftM2等内容,以及liftM3和朋友。