在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
当前回答
实际上,monad基本上允许回调嵌套(具有相互递归的线程状态(请忽略连字符))(以可组合(或可分解)的方式)(具有类型安全性(有时(取决于语言))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
例如,这不是单子:
//JavaScript is 'Practical'
var getAllThree =
bind(getFirst, function(first){
return bind(getSecond,function(second){
return bind(getThird, function(third){
var fancyResult = // And now make do fancy
// with first, second,
// and third
return RETURN(fancyResult);
});});});
但是monad启用了这样的代码。monad实际上是一组类型:{bind,RETURN,也许其他我不认识的人…}。这本质上是无关紧要的,实际上是不切实际的。
所以现在我可以使用它:
var fancyResultReferenceOutsideOfMonad =
getAllThree(someKindOfInputAcceptableToOurGetFunctionsButProbablyAString);
//Ignore this please, throwing away types, yay JavaScript:
// RETURN = K
// bind = \getterFn,cb ->
// \in -> let(result,newState) = getterFn(in) in cb(result)(newState)
或将其分解:
var getFirstTwo =
bind(getFirst, function(first){
return bind(getSecond,function(second){
var fancyResult2 = // And now make do fancy
// with first and second
return RETURN(fancyResult2);
});})
, getAllThree =
bind(getFirstTwo, function(fancyResult2){
return bind(getThird, function(third){
var fancyResult3 = // And now make do fancy
// with fancyResult2,
// and third
return RETURN(fancyResult3);
});});
或者忽略某些结果:
var getFirstTwo =
bind(getFirst, function(first){
return bind(getSecond,function(second){
var fancyResult2 = // And now make do fancy
// with first and second
return RETURN(fancyResult2);
});})
, getAllThree =
bind(getFirstTwo, function(____dontCare____NotGonnaUse____){
return bind(getThird, function(three){
var fancyResult3 = // And now make do fancy
// with `three` only!
return RETURN(fancyResult3);
});});
或者从以下内容简化一个小案例:
var getFirstTwo =
bind(getFirst, function(first){
return bind(getSecond,function(second){
var fancyResult2 = // And now make do fancy
// with first and second
return RETURN(fancyResult2);
});})
, getAllThree =
bind(getFirstTwo, function(_){
return bind(getThird, function(three){
return RETURN(three);
});});
收件人(使用“正确身份”):
var getFirstTwo =
bind(getFirst, function(first){
return bind(getSecond,function(second){
var fancyResult2 = // And now make do fancy
// with first and second
return RETURN(fancyResult2);
});})
, getAllThree =
bind(getFirstTwo, function(_){
return getThird;
});
或者把它们挤在一起:
var getAllThree =
bind(getFirst, function(first_dontCareNow){
return bind(getSecond,function(second_dontCareNow){
return getThird;
});});
这些能力的实用性并没有真正显现出来,或者变得清晰,直到你试图解决真正的棘手问题例如解析或模块/ajax/资源加载。
你能想象成千上万行indexOf/subString逻辑吗?如果频繁的解析步骤包含在小函数中呢?像字符、空格、大写字符或数字这样的函数?如果这些函数在回调中给出了结果,而不必与Regex集团和争论发生冲突?如果它们的组成/分解被很好地理解了呢?这样你就可以从下往上构建大型解析器了吗?
因此,管理嵌套回调范围的能力非常实用,尤其是在使用monadic解析器组合器库时。(也就是说,根据我的经验)
不要挂断电话:-分类理论-可能是月-莫纳德定律-哈斯克尔- !!!!
其他回答
第一:如果你不是数学家,monad这个词有点空洞。另一个术语是计算构建器,它更能描述它们的实际用途。
它们是链接操作的模式。它看起来有点像面向对象语言中的方法链接,但机制略有不同。
该模式主要用于函数式语言(特别是Haskell,它普遍使用monad),但也可以用于支持高阶函数的任何语言(即可以将其他函数作为参数的函数)。
JavaScript中的数组支持该模式,因此让我们将其作为第一个示例。
模式的要点是我们有一个类型(在本例中为Array),它有一个以函数作为参数的方法。提供的操作必须返回相同类型的实例(即返回数组)。
首先是一个不使用monad模式的方法链接示例:
[1,2,3].map(x => x + 1)
结果是[2,3,4]。代码不符合monad模式,因为我们作为参数提供的函数返回的是数字,而不是数组。monad形式的相同逻辑是:
[1,2,3].flatMap(x => [x + 1])
这里我们提供了一个返回Array的操作,所以现在它符合模式。flatMap方法为数组中的每个元素执行提供的函数。它期望每个调用都有一个数组作为结果(而不是单个值),但将得到的数组集合并为一个数组。所以最终的结果是相同的,数组[2,3,4]。
(提供给map或flatMap等方法的函数参数在JavaScript中通常称为“回调”。我将其称为“操作”,因为它更通用。)
如果我们连锁多个操作(以传统方式):
[1,2,3].map(a => a + 1).filter(b => b != 3)
数组中的结果[2,4]
monad形式的相同链接:
[1,2,3].flatMap(a => [a + 1]).flatMap(b => b != 3 ? [b] : [])
产生相同的结果,即数组[2,4]。
您将立即注意到monad格式比非monad格式更难看!这正好表明单子不一定“好”。它们是一种有时有益有时不有益的模式。
请注意,monad模式可以以不同的方式组合:
[1,2,3].flatMap(a => [a + 1].flatMap(b => b != 3 ? [b] : []))
这里的绑定是嵌套的,而不是链式的,但结果是一样的。这是单子的一个重要属性,我们稍后会看到。这意味着组合的两个操作可以被视为单个操作。
该操作允许返回具有不同元素类型的数组,例如,将数字数组转换为字符串数组或其他东西;只要它仍然是一个数组。
这可以使用Typescript表示法更正式地描述。数组的类型为array<T>,其中T是数组中元素的类型。flatMap()方法接受类型为T=>Array<U>的函数参数,并返回一个Array<U>。
一般来说,monad是任何类型的Foo<Bar>,它有一个“bind”方法,该方法接受类型为Bar=>Foo<Baz>的函数参数,并返回一个Foo<Baz>。
这回答了单子是什么。这个答案的其余部分将试图通过示例来解释为什么monads在Haskell这样的语言中是一种有用的模式,而Haskell对monads有很好的支持。
Haskell和Do表示法
要将map/filter示例直接转换为Haskell,我们将flatMap替换为>>=运算符:
[1,2,3] >>= \a -> [a+1] >>= \b -> if b == 3 then [] else [b]
>>=运算符是Haskell中的绑定函数。当操作数是一个列表时,它与JavaScript中的flatMap相同,但对于其他类型,它被重载了不同的含义。
但是Haskell还为monad表达式提供了专用语法do块,它完全隐藏了绑定运算符:
do a <- [1,2,3]
b <- [a+1]
if b == 3 then [] else [b]
这将隐藏“管道”,并让您专注于在每个步骤中应用的实际操作。
在do块中,每一行都是一个操作。约束仍然认为块中的所有操作都必须返回相同的类型。因为第一个表达式是一个列表,所以其他操作也必须返回一个列表。
向后箭头<-看起来像赋值,但请注意,这是绑定中传递的参数。因此,当右侧的表达式是整数列表时,左侧的变量将是一个整数,但将对列表中的每个整数执行。
示例:安全导航(Maybe类型)
关于列表,让我们来看看monad模式如何对其他类型有用。
某些函数可能不总是返回有效值。在Haskell中,这由Maybe类型表示,该类型是Just value或Nothing选项。
总是返回有效值的链接操作当然很简单:
streetName = getStreetName (getAddress (getUser 17))
但如果任何函数都可以返回Nothing呢?我们需要单独检查每个结果,如果不是Nothing,则只将值传递给下一个函数:
case getUser 17 of
Nothing -> Nothing
Just user ->
case getAddress user of
Nothing -> Nothing
Just address ->
getStreetName address
很多重复检查!想象一下如果链条更长。Haskell用Maybe的monad模式解决了这个问题:
do
user <- getUser 17
addr <- getAddress user
getStreetName addr
这个do块调用Maybe类型的绑定函数(因为第一个表达式的结果是Maybe)。绑定函数仅在值为Just值时执行以下操作,否则只传递Nothing。
这里使用monad模式来避免重复代码。这与其他一些语言使用宏来简化语法的方式类似,尽管宏以非常不同的方式实现了相同的目标。
请注意,Haskell中monad模式和monad友好语法的结合导致了代码更干净。在JavaScript这样的语言中,如果没有对monad的任何特殊语法支持,我怀疑monad模式是否能够在这种情况下简化代码。
可变状态
Haskell不支持可变状态。所有变量都是常量,所有值都是不可变的。但State类型可用于模拟具有可变状态的编程:
add2 :: State Integer Integer
add2 = do
-- add 1 to state
x <- get
put (x + 1)
-- increment in another way
modify (+1)
-- return state
get
evalState add2 7
=> 9
add2函数构建一个monad链,然后以7作为初始状态对其求值。
显然,这在Haskell中才有意义。其他语言支持开箱即用的可变状态。Haskell通常在语言特性上是“选择加入”的——您可以在需要时启用可变状态,并且类型系统确保效果是显式的。IO是这方面的另一个例子。
IO
IO类型用于链接和执行“不纯”函数。
与任何其他实用语言一样,Haskell有一系列与外界接口的内置函数:putStrLine、readLine等。这些函数被称为“不纯”,因为它们要么会产生副作用,要么会产生不确定性的结果。即使是像获取时间这样简单的事情也被认为是不纯洁的,因为结果是不确定的——用相同的参数调用两次可能会返回不同的值。
纯函数是确定性的——它的结果完全取决于传递的参数,除了返回值之外,它对环境没有任何副作用。
Haskell大力鼓励使用纯函数——这是该语言的一个主要卖点。不幸的是,对于纯粹主义者来说,你需要一些不纯的函数来做任何有用的事情。Haskell折衷方案是将纯函数和不纯函数彻底分开,并保证纯函数无法直接或间接执行不纯函数。
这是通过给所有不纯函数赋予IO类型来保证的。Haskell程序的入口点是具有IO类型的主函数,因此我们可以在顶层执行不纯的函数。
但是该语言如何防止纯函数执行不纯函数?这是因为Haskell的懒惰本性。只有当某个函数的输出被其他函数消耗时,才执行该函数。但除了将IO值分配给main之外,没有办法使用它。因此,如果一个函数想要执行一个不纯的函数,它必须连接到main并具有IO类型。
对IO操作使用monad链接还可以确保它们以线性和可预测的顺序执行,就像命令式语言中的语句一样。
这让我们看到大多数人会用Haskell编写的第一个程序:
main :: IO ()
main = do
putStrLn ”Hello World”
当只有一个操作,因此没有什么要绑定时,do关键字是多余的,但为了保持一致性,我还是保留了它。
()类型表示“无效”。这种特殊的返回类型仅适用于因其副作用而调用的IO函数。
更长的示例:
main = do
putStrLn "What is your name?"
name <- getLine
putStrLn "hello" ++ name
这构建了一个IO操作链,因为它们被分配给主功能,所以它们被执行。
将IO与Maybe进行比较表明了monad模式的多功能性。对于Maybe,该模式用于通过将条件逻辑移动到绑定函数来避免重复代码。对于IO,该模式用于确保IO类型的所有操作都是有序的,并且IO操作不会“泄漏”到纯函数。
总结
在我的主观看法中,monad模式只有在对该模式有一些内置支持的语言中才真正有价值。否则,它只会导致过于复杂的代码。但是Haskell(和其他一些语言)有一些内置支持,隐藏了繁琐的部分,然后该模式可以用于各种有用的事情。喜欢:
避免重复代码(可能)为程序的分隔区域添加可变状态或异常等语言特性。将讨厌的东西与美好的东西隔离开来(IO)嵌入式域特定语言(解析器)将GOTO添加到语言中。
Monad是一个可应用的(即,你可以将二进制(因此,“n元”)函数提升到(1),并将纯值注入(2))Functor(即,可以映射到(3)的函数,即提升一元函数到(3”),它还具有展平嵌套数据类型的能力(三个概念中的每一个都遵循其相应的一组规则)。在Haskell中,这种扁平化操作称为join。
此“联接”操作的常规(通用、参数化)类型为:
join :: Monad m => m (m a) -> m a
对于任何monad m(注意,类型中的所有ms都是相同的!)。
特定的m monad定义了其特定版本的join,该版本适用于由类型m A的monadic值“携带”的任何值类型A。某些特定类型包括:
join :: [[a]] -> [a] -- for lists, or nondeterministic values
join :: Maybe (Maybe a) -> Maybe a -- for Maybe, or optional values
join :: IO (IO a) -> IO a -- for I/O-produced values
连接操作将产生a型值的m计算的m计算转换为a型值组合的m计算。这允许将计算步骤组合成一个更大的计算。
结合“bind”(>>=)运算符的计算步骤简单地使用fmap和join,即。
(ma >>= k) == join (fmap k ma)
{-
ma :: m a -- `m`-computation which produces `a`-type values
k :: a -> m b -- create new `m`-computation from an `a`-type value
fmap k ma :: m ( m b ) -- `m`-computation of `m`-computation of `b`-type values
(m >>= k) :: m b -- `m`-computation which produces `b`-type values
-}
相反,可以通过bind定义join,join mma==join(fmap id mma)==mma>>=id,其中id ma=ma——对于给定的类型m,以更方便的为准。
对于monad,do表示法及其使用代码的等效绑定,
do { x <- mx ; y <- my ; return (f x y) } -- x :: a , mx :: m a
-- y :: b , my :: m b
mx >>= (\x -> -- nested
my >>= (\y -> -- lambda
return (f x y) )) -- functions
可以读为
首先“做”mx,当它完成时,将其“结果”作为x,让我用它“做”其他事情。
在给定的do块中,绑定箭头<-右侧的每个值对于某些类型a都是m a类型,在整个do块中都是相同的monad m。
返回x是一个中立的m计算,它只产生给定的纯值x,因此将任何m计算与返回绑定都不会改变该计算。
(1) 提升A2::适用m=>(a->b->c)->m a->m b->m c
(2) 纯::适用m=>a->m a
(3) 具有fmap::函数m=>(a->b)->m a->m b
还有等效的Monad方法,
liftM2 :: Monad m => (a -> b -> c) -> m a -> m b -> m c
return :: Monad m => a -> m a
liftM :: Monad m => (a -> b) -> m a -> m b
给定monad,其他定义可以如下
pure a = return a
fmap f ma = do { a <- ma ; return (f a) }
liftA2 f ma mb = do { a <- ma ; b <- mb ; return (f a b) }
(ma >>= k) = do { a <- ma ; b <- k a ; return b }
一个非常简单的答案是:
Monad是一种抽象,它为封装值、计算新的封装值和展开封装值提供了接口。
它们在实践中的方便之处在于,它们提供了一个统一的接口,用于创建建模状态而非状态的数据类型。
必须理解Monad是一种抽象,即用于处理某种数据结构的抽象接口。然后,该接口用于构建具有一元行为的数据类型。
你可以在Ruby中的Monads中找到一个非常好且实用的介绍,第1部分:简介。
正如丹尼尔·斯皮瓦克(Daniel Spiewak)所解释的,修道院不是隐喻,而是从一种共同模式中产生的一种实用的抽象。
如果你要求对如此抽象的东西做出简洁、实用的解释,那么你只能希望得到一个抽象的答案:
a -> b
是表示从as到bs的计算的一种方式。您可以将计算链接起来,也可以将它们组合在一起:
(b -> c) -> (a -> b) -> (a -> c)
更复杂的计算需要更复杂的类型,例如:
a -> f b
是从as到bs到fs的计算类型。您还可以编写它们:
(b -> f c) -> (a -> f b) -> (a -> f c)
事实证明,这种模式无处不在,并且与上面的第一个组合具有相同的财产(结合性、右-和左-同一性)。
人们必须给这个模式起一个名字,但如果知道第一个组合被正式描述为半群体,这会有帮助吗?
“单子和圆括号一样有趣和重要”(奥列格·基斯廖夫)