monad 定义中的样板代码

Boilerplate code in the definition of a monad

Functor-Applicative-Monad Proposal 以来,Monad 是 Applicative 的子类,Applicative 又是 Functor 的子类。从数学上讲,这似乎是一个明智的选择,我对此没有任何问题。

然而,令我恼火的是,即使 fmappure<*> 的相应法则被固定,也需要写下 Functor 和 Applicative 实例无论如何,单子法则。事实上,在上面的链接提案中,他们自己写道:“您可以通过添加以下代码从 Monad 中简单地派生这些实例”:

import Control.Applicative (Applicative(..))
import Control.Monad       (liftM, ap)

instance Functor m where
    fmap = liftM

instance Applicative m where
    pure  = {- move the definition of `return` from the `Monad` instance here -}
    (<*>) = ap

在大多数教程中,您只会看到等式 pure=return,然后人们会像以前一样在 Monad 实例中继续定义 return。因此,实际上,为只想定义 Monad 实例的每个人添加了 10 行样板代码。

在数学中,通常也不是这样运算的。例如,在定义某个环的合成定律时,reader 通常不会再次明确提醒环是加法下阿贝尔群的特例,而阿贝尔群又是群的特例。无论如何,这在定义上是明确的。

因此我的问题是:尽管 Monad 是 Applicative 的子类很好,而 Applicative 又是 Functor 的子类,但从编译器插入样板代码而不是程序员?

文档中的建议适用于过去定义了 Monad 实例但没有 Applicative 实例的遗留类型。然后根据 现有的 Monad 定义这些实例 是使包再次编译的快速可靠的修复方法。

但是,这不是新代码应该采用的方式,因为 Monad 实际上是更高级的 class。推荐的方式是

  1. 派生 Functor 实例。始终只有一种可能的方法可以做到这一点,编译器可以为您完成。

    {-# LANGUAGE DeriveFunctor #-}
    data YourMonadType a = ...
      deriving (Functor)
    
  2. 定义 Applicative。这不是很自动,有时 <*> 可能有点违反直觉,但您会掌握它的窍门,直接实施实际上比 (<*>) = ap.

    更有效
    instance Applicative YourMonadType where
      pure = {- direct definition here -}
      q <*> r = {- direct definition here -}
    
  3. 定义 Monad。这根本不需要 return(因为它是默认的 = pure),只需要 >>=.

    instance Monad YourMonadType where
      q >>= f = {- direct definition here -}
    

    旁注:可以说 Monad 的方法实际上应该是 join 而不是 >>=。虽然 return>>= 产生了所有 FunctorApplicative 方法,但 join 是它们的“正交特征”。


在现代 GHC 中,也可以通过在 WrappedMonad 新类型上使用 via 策略来获得 Applicative 的派生实例:

{-# LANGUAGE DerivingVia, DeriveFunctor #-}

import Control.Applicative

data YourMonadType a = YourMonadType a a
  deriving stock (Functor, Show)
  deriving Applicative via (WrappedMonad YourMonadType)

instance Monad YourMonadType where
  return x = YourMonadType x x
  YourMonadType x y >>= f = YourMonadType fx fy
   where YourMonadType fx _ = f x
         YourMonadType _ fy = f y
ghci> YourMonadType (+1) (*2) <*> YourMonadType 10 20
YourMonadType 11 40