Monads有什么特别之处?

monad是一个数学结构,主要用于(纯)函数式编程,基本上是Haskell。 但是,还有许多其他数学结构可用,例如应用函子,强单子或单子。 有些更具体,有些更通用。 然而,单子更受欢迎。 这是为什么?

我提出的一个解释是,它们是通用性和特异性之间的一个甜蜜点。 这意味着单子能够捕捉足够的关于数据的假设,以应用我们通常使用的算法以及我们通常满足一元法则的数据。

另一种解释可能是,Haskell提供monads(do-notation)的语法,但不适用于其他结构,这意味着Haskell程序员(以及函数式编程研究人员)可以直观地吸引单子,其中更通用或特定(高效)函数工作也是如此。


我怀疑,对这个特定类型( Monad )与其他许多其他类型的过度关注主要是历史偶然。 人们经常将IOMonad联系起来,尽管这两者是独立有用的想法(就像列表翻转和香蕉一样)。 因为IO是神奇的(有一个实现,但没有表示), Monad通常与IO相关联,所以很容易陷入对Monad神奇想法。

(另外: IO甚至是否是monad是值得怀疑的,单子法是否成立?法律甚至对IO来说意味着什么,即平等意味着什么?注意与国家monad有问题的关联。)


如果类型m :: * -> *具有Monad实例,则可以使用类型为a -> mb图灵完整函数组合。 这是一个非常有用的属性。 您可以将各种图灵完备的控制流程从特定的含义中抽象出来。 这是一个最小的组合模式,支持抽象任何控制流来处理支持它的类型。

比较这个与Applicative 。 在那里,你只能得到具有相当于下推自动机的计算能力的组合模式。 当然,更多类型支持具有更多有限权力的组合是事实。 确实,当你限制可用功率时,你可以进行额外的优化。 这两个原因是Applicative类存在并且有用的原因。 但是,通常可以是Monad实例的东西,以便该类型的用户可以使用该类型执行最常用的操作。

编辑:按照大众需求,这里有一些使用Monad类的功能:

ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM c x y = c >>= z -> if z then x else y

whileM :: Monad m => (a -> m Bool) -> (a -> m a) -> a -> m a
whileM p step x = ifM (p x) (step x >>= whileM p step) (return x)

(*&&) :: Monad m => m Bool -> m Bool -> m Bool
x *&& y = ifM x y (return False)

(*||) :: Monad m => m Bool -> m Bool -> m Bool
x *|| y = ifM x (return True) y

notM :: Monad m => m Bool -> m Bool
notM x = x >>= return . not

将这些与do语法(或原始的>>=运算符)结合起来,可以为您提供名称绑定,无限循环和完整的布尔逻辑。 这是一套众所周知的足以赋予图灵完备性的基元。 请注意,所有函数如何取消单值函数,而不是简单的值。 所有monadic效果只有在必要时才会被绑定 - 只有来自ifM选定分支的效果ifM绑定到其最终值中。 *&&*|| 在可能的时候忽略他们的第二个论点 等等..

现在,这些类型签名可能不涉及每个一元操作数的函数,但这只是一种认知简化。 如果所有非函数参数和结果都更改为() -> ma ,那么忽略底部将不会有语义差异。 对于用户来说,优化认知开销是非常友好的。

现在,让我们看看使用Applicative接口的那些函数会发生什么。

ifA :: Applicative f => f Bool -> f a -> f a -> f a
ifA c x y = (c' x' y' -> if c' then x' else y') <$> c <*> x <*> y

恩,呃。 它有相同的类型签名。 但这里已经有一个非常大的问题了。 无论选择哪一个值,x和y的作用都被绑定到组合结构中。

whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
whileA p step x = ifA (p x) (whileA p step <$> step x) (pure x)

那么,好吧,这似乎是好的,除了它是一个无限循环的事实,因为ifA将始终执行两个分支......除非它不是那么接近。 pure x的类型为fawhileA p step <$> step x的类型为f (fa) 。 这甚至不是一个无限循环。 这是一个编译错误。 让我们再试一次..

whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
whileA p step x = ifA (p x) (whileA p step <*> step x) (pure x)

拍好。 甚至没有那么远。 whileA p step的类型a -> fa 。 如果您尝试将它用作<*>的第一个参数,它将抓取顶部类型构造函数的Applicative实例,它是(->) ,而不是f 。 是的,这也不会奏效。

事实上,我的Monad示例中唯一可以与Applicative接口配合使用的功能不是notM 事实上,该特定功能仅适用于Functor界面。 其余的部分? 他们失败了。

当然,可以预期,您可以使用Monad接口编写代码,而无法使用Applicative接口。 毕竟,它更加强大。 但有趣的是你失去了什么。 你失去了编写功能的能力,这些功能根据他们的输入改变了他们所具有的效果。 也就是说,你丧失了编写某些控制流模式的能力,这些模式构成了类型为a -> fb函数。

图灵完整的组合正是Monad界面令人感兴趣的原因。 如果它不允许图灵完整组合,那么程序员就不可能将IO操作组合在任何特定的控制流中,而这些控制流并没有很好地预先包装好。 事实是,您可以使用Monad原语来表达任何控制流,这使得IO类型成为管理Haskell中的IO问题的一种可行的方法。

IO更多的类型具有语义上有效的Monad接口。 恰巧Haskell有语言工具可以在整个界面上进行抽象。 由于这些因素, Monad在可能的情况下提供实例。 这样做可以让您访问为单一类型提供的所有现有抽象功能,而不管具体类型是什么。

因此,如果Haskell程序员似乎总是关心某个类型的Monad实例,那是因为它是可以提供的最通用的实例。


首先,我认为monad比任何其他更受欢迎并不是真的, Functor和Monoid都有很多不是monad的实例。 但他们都非常具体; Functor提供映射,Monoid级联。 应用程序是我可以想到的一个类,由于其相当大的权力,可能未被充分利用,很大程度上是因为它是一种相对较新的语言。

但是,单子非常受欢迎。 其中的一部分是符号; 很多Monoid提供了Monad实例,它们只是将值附加到正在运行的累加器(本质上是一个隐式编写器)。 blaze-html库就是一个很好的例子。 我认为,原因是类型签名(>>=) :: Monad m => ma -> (a -> mb) -> mb的威力。 虽然fmap和mappend是有用的,但他们能做的事情是相当狭隘的。 然而,绑定可以表达各种各样的事物。 当然,它是在IO monad中标准化的,也许是在流和FRP之前对IO进行最好的纯粹的功能性方法(对于简单任务和定义组件,它们在它们旁边仍然很有用)。 但它也提供了隐式状态(Reader / Writer / ST),它可以避免一些非常繁琐的变量传递。 尤其是,各种状态monad非常重要,因为它们提供了状态为单线程的保证,从而在融合之前允许纯(非IO)代码中的可变结构。 但绑定有一些更奇特的用途,例如扁平化嵌套的数据结构(List和Set monads),这两者在他们的位置非常有用(我通常会看到它们使用desugared,显式调用liftM或(>> =),所以它不是符号的问题)。 因此,尽管Functor和Monoid(以及比较罕见的Foldable,Alternative,Traversable等)为相当简单的函数提供了标准化接口,但Monad的绑定相当灵活。

总之,我认为你所有的理由都有一定的作用。 Monad的流行是由于历史事故(符号和适用范围的后期定义)以及它们与权力和一般性(相对于函数,单片机等)以及可理解性(相对于箭头)的组合的结合。

链接地址: http://www.djcxy.com/p/7431.html

上一篇: What is so special about Monads?

下一篇: When to use Haskell monads