在定义Functor时,是更好使用Applicative还是Monad,或者反过来?

18

这是一个普遍性的问题,不涉及具体的代码。

假设你有一个类型为T a的对象,可以为它提供一个Monad实例。由于每个单子都是通过赋值pure = return(<*>) = ap而成为了Applicative,然后每个应用程序都是通过fmap f x = pure f <*> x成为了Functor,那么定义Monad实例然后再给T应用ApplicativeFunctor实例是否更好呢?

对我来说,这感觉有点反过来了。如果我在做数学而不是编程,我会认为我首先要证明我的对象是一个functor,然后继续添加限制,直到我也证明它是一个monad。我知道Haskell仅仅是受范畴论启发而已,显然,在构建证明时使用的技巧与编写有用程序时使用的技巧不同,但我想从Haskell社区得到一个意见。是从MonadFunctor更好呢?还是从FunctorMonad更好呢?

5个回答

25

我倾向于先编写并查看Functor实例,尤其是如果您使用LANGUAGE DeriveFunctor编译指示,则data Foo a = Foo a deriving (Functor)在大多数情况下都有效。

当您的Applicative可以比您的Monad更通用时,一些棘手的问题涉及实例的协议。例如,这里有一个Err数据类型:

data Err e a = Err [e] | Ok a deriving ( Functor )

instance Applicative (Err e) where
  pure = Ok
  Err es <*> Err es' = Err (es ++ es')
  Err es <*> _       = Err es
  _      <*> Err es  = Err es
  Ok  f  <*> Ok  x   = Ok (f x)

instance Monad (Err e) where
  return = pure
  Err es >>= _ = Err es
  Ok  a  >>= f = f a

上面我按照 Functor-to-Monad 的顺序定义了实例,就单独而言,每个实例都是正确的。不幸的是,ApplicativeMonad 实例并不相符:观察性质不同的函数有 ap(<*>) 以及 (>>)(*>)

Err "hi" <*>  Err "bye" == Err "hibye"
Err "hi" `ap` Err "bye" == Err "hi"

为了使代码更加合理,特别是当每个人都使用应用型函子/单子提案时,这些应该保持一致。如果你定义了 instance Applicative (Err e) where { pure = return; (<*>) = ap } 那么它们就保持一致。

但是,最后,您可能能够仔细地分离出ApplicativeMonad之间的差异,以使它们以不同的方式表现出来,例如具有更懒惰或更高效的Applicative实例。这实际上经常发生,我觉得评判“benign”在何种情况下应该对齐您的实例,还有一点争议。也许其中最活跃的使用是在Facebook的Haxl项目中,其中Applicative实例比Monad实例更加并行化,因此在牺牲一些相当严重的“未观察到”的副作用的代价下,效率更高。

无论如何,如果它们不同,请记录下来。


6

与Abrahamson的答案相比,我经常选择一种反向方法。我手动定义Monad实例,并使用Control.Monad中已定义的函数将ApplicativeFunctor定义为它的术语,从而使这些实例对于任何单子都是相同的,即:

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

instance Functore SomeMonad where
  fmap = liftM

虽然这种方式定义 FunctorApplicative 总是“无脑”的,而且非常容易理解,但我必须指出,这并不是最终的解决方案,因为有些情况下,实例的实现可能更高效,甚至提供新功能。例如,ConcurrentlyApplicative 实例可以同时执行任务,而由于单调性的本质, Monad 实例只能按顺序执行它们。

要明确的是,我并不认为这是一种不好的方式。我只是强调“F->A->M”方法的优点和缺点。个人而言,我倾向于按顺序定义最容易正确的函数。 - J. Abrahamson
我认为 E.Kmett 在 haskellcast 中说过,将会有一个变化,即Monad将成为Applicative的子类,但我不确定这个变化确切发生的时间是什么时候。 - epsilonhalbe
2
@epsilonhalbe: 这与实例定义之间的依赖关系没有任何关系,所以这相当无关紧要。 - leftaroundabout
2
唯一的问题是有时这些方法不如手写版本高效。列表单子是一个很好的例子,其中liftM比没有重写规则的手写map要低效。 - Gabriella Gonzalez

3

Functor实例通常很容易定义,我通常会手动完成。

对于ApplicativeMonad,情况不同。 purereturn通常同样简单,并且扩展定义放在哪个类中并不重要。对于bind,有时候采用“范畴方式”更有利,即首先定义一个专门的join' :: (M (M x)) -> M x,然后a>>=b = join' $ fmap b a(如果你是以>>=来定义fmap,这当然行不通)。然后为Applicative实例重复使用(>>=)可能很有用。

有时,Applicative 实例可以非常容易地编写或比通用的基于 Monad 的实现更有效率。在这种情况下,你应该明确定义 <*>

0

我认为你误解了Haskell中子类的工作方式。它们不像面向对象的子类!相反,子类约束,例如

class Applicative m => Monad m

说“任何带有规范Monad结构的类型也必须具有规范的Applicative结构”。将这样的约束放置在代码中有两个基本原因:
  • 子类结构引入了超类结构。
  • 超类结构是子类结构的自然子集。
例如,请考虑:
class Vector v where
    (.^) :: Double -> v -> v
    (+^) :: v -> v -> v
    negateV :: v -> v

class Metric a where
    distance :: a -> a -> Double

class (Vector v, Metric v) => Norm v where
    norm :: v -> Double

Norm 的第一个超类约束是因为除非您还假定向量空间结构,否则范数空间的概念实际上很弱;第二个约束是因为(在给定向量空间的情况下)Norm 会引出一个 Metric,您可以通过观察证明这一点。

instance Metric V where
    distance v0 v1 = norm (v0 .^ negateV v1)

对于任何具有有效向量实例和有效范数函数的V,都是有效的Metric实例。我们说范数引导了度量。请参见http://en.wikipedia.org/wiki/Normed_vector_space#Topological_structure

Monad上的FunctorApplicative超类类似于Metric,而不是Vector:来自Monadreturn>>=函数引导了FunctorApplicative结构:

  • fmap:可以定义为fmap f a = a >>= return . f,这在Haskell 98标准库中是liftM
  • pure:与return相同;这两个名称是遗留问题,当时Applicative不是Monad的超类。
  • <*>:可以定义为af <*> ax = af >>= \ f -> ax >>= \ x -> return (f x),这在Haskell 98标准库中是liftM2 ($)
  • join:可以定义为join aa = aa >>= id

因此,在数学上,用Monad来定义FunctorApplicative操作是完全合理的。


0

这里的魔法是,Haskell使用单子的Kleisli三元组符号,这是一种更方便的方式,如果有人想在命令式编程中使用单子作为工具。

我问了同样的问题,答案过了一会儿才出现,如果你看到了Haskell中FunctorApplicativeMonad的定义,你会发现一个链接缺失了,那就是单子的原始定义,其中只包含了join操作,可以在HaskellWiki上找到。

从这个角度来看,你会发现Haskell单子是由函子、可应用函子、单子和Kliesli三元组构建而成的。

这里可以找到一个简单的解释:https://github.com/andorp/pearls/blob/master/Monad.hs 还有其他相同想法的内容在这里:http://people.inf.elte.hu/pgj/haskell2/jegyzet/08/Monad.hs


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接