Monads有什么特别之处?
monad是一个数学结构,主要用于(纯)函数式编程,基本上是Haskell。 但是,还有许多其他数学结构可用,例如应用函子,强单子或单子。 有些更具体,有些更通用。 然而,单子更受欢迎。 这是为什么?
我提出的一个解释是,它们是通用性和特异性之间的一个甜蜜点。 这意味着单子能够捕捉足够的关于数据的假设,以应用我们通常使用的算法以及我们通常满足一元法则的数据。
另一种解释可能是,Haskell提供monads(do-notation)的语法,但不适用于其他结构,这意味着Haskell程序员(以及函数式编程研究人员)可以直观地吸引单子,其中更通用或特定(高效)函数工作也是如此。
我怀疑,对这个特定类型( Monad
)与其他许多其他类型的过度关注主要是历史偶然。 人们经常将IO
与Monad
联系起来,尽管这两者是独立有用的想法(就像列表翻转和香蕉一样)。 因为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
的类型为fa
。 whileA 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