在最近简要回顾了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是一种函数组合技术,它使用组合函数bind将某些输入场景的处理具体化,以在组合过程中预处理输入。
在正常合成中,函数compose(>>)用于按顺序将合成的函数应用于其前身的结果。重要的是,所组成的函数需要处理其输入的所有场景。
(x->y)>>(y->z)
这种设计可以通过重组输入来改进,以便更容易地询问相关状态。因此,如果y包含有效性的概念,则值可以变成Mb,例如(is_OK,b),而不是简单的y。
例如,当输入仅可能是一个数字时,而不是返回一个可以尽职尽责地包含数字或不包含数字的字符串,您可以将类型重新构造为bool,以指示元组中存在有效数字和数字,例如bool*float。组合函数现在不再需要解析输入字符串来确定数字是否存在,而只需要检查元组的布尔部分。
(Ma->Mb)>>(Mb->Mc)
在这里,合成与合成一起自然发生,因此每个函数必须单独处理其输入的所有场景,尽管现在这样做要容易得多。
然而,如果我们能够将审讯的工作外化,以应对那些处理场景是常规的情况,那又会怎样呢。例如,如果我们的程序在输入不正常时什么都不做,比如is_OK为false时。如果做到了这一点,那么组合函数就不需要自己处理该场景,从而大大简化了代码并实现了另一个级别的重用。
为了实现这种外部化,我们可以使用bind(>>=)函数来执行组合而不是组合。因此,不是简单地将值从一个函数的输出传递到另一个函数输入,而是检查Ma的M部分,并决定是否以及如何将组合函数应用于a。当然,函数绑定将专门为我们的特定M定义,以便能够检查其结构并执行我们想要的任何类型的应用。尽管如此,a可以是任何东西,因为bind仅在确定应用程序需要时将未检查的a传递给组合函数。此外,组合函数本身也不再需要处理输入结构的M部分,从而简化了它们。因此
(a->Mb)>>=(b->Mc)或更简洁地Mb>>=
简言之,一旦输入被设计为充分暴露某些输入场景,monad就外部化了,从而提供了关于处理这些输入场景的标准行为。这种设计是一种外壳和内容模型,其中外壳包含与组合函数的应用程序相关的数据,并由绑定函数查询,并且仅对绑定函数可用。
因此,单子是三件事:
M外壳,用于保存monad相关信息,实现的绑定函数,用于在将组合函数应用于其在外壳中找到的内容值时使用该外壳信息,以及形式为a->Mb的可组合函数,生成包含单元管理数据的结果。
一般来说,函数的输入比其输出更具限制性,其中可能包括错误条件等;因此,Mb结果结构通常非常有用。例如,当除数为0时,除法运算符不返回数字。
此外,monad可以包括将值a包装成monadic类型Ma的包装函数,以及将一般函数a->b包装成monodic函数a->Mb的包装函数。当然,像bind一样,这样的包装函数是M特有的。例如:
let return a = [a]
let lift f a = return (f a)
绑定函数的设计假定了不可变的数据结构和纯函数,其他事情变得复杂,无法保证。因此,有一元定律:
鉴于
M_
return = (a -> Ma)
f = (a -> Mb)
g = (b -> Mc)
然后
Left Identity : (return a) >>= f === f a
Right Identity : Ma >>= return === Ma
Associative : Ma >>= (f >>= g) === Ma >>= ((fun x -> f x) >>= g)
关联性意味着无论何时应用绑定,绑定都会保留求值顺序。也就是说,在上述关联性的定义中,对f和g的括号化绑定的强制早期评估只会导致期望Ma的函数完成绑定。因此,必须先确定Ma的值,然后才能将其值应用于f,进而将结果应用于g。
Monad是一种带有特殊机器的盒子,它允许你从两个嵌套的盒子中制作一个普通的盒子,但仍然保持两个盒子的一些形状。
具体来说,它允许您执行连接,类型为Monad m=>m(m a)->m a。
它还需要一个返回操作,它只包装一个值。return::Monad m=>a->m a你也可以说joinunboxes和return wrappes,但join不是Monad m=>m a->a类型的(它不会打开所有Monad,而是打开Monad,Monad在其中)
所以它取一个Monad盒子(Monad m=>,m),里面有一个盒子((m a)),然后生成一个普通盒子(m a。
然而,Monad通常用于(>>=)(口语“bind”)运算符,它本质上只是一个fmap和一个接一个的join。具体而言,
x >>= f = join (fmap f x)
(>>=) :: Monad m => (a -> m b) -> m a -> m b
请注意,函数出现在第二个参数中,而不是fmap。
此外,join=(>>=id)。
为什么这有用?本质上,它允许您在某种框架(Monad)中工作时制作将动作串在一起的程序。
Haskell中Monad的最突出用途是IO Monad。现在,IO是对Haskell中的Action进行分类的类型。在这里,Monad系统是唯一的保存方式(华丽的词):
参考透明度懒惰纯洁
本质上,像getLine::IOString这样的IO操作不能被String替换,因为它总是具有不同的类型。把IO想象成一种神奇的盒子,可以把东西传送给你。然而,仍然只是说getLine::IOString和所有函数都接受IOa会导致混乱,因为可能不需要这些函数。const“üp§”getLine会做什么?(const丢弃第二个参数。const a b=a。)getLine不需要求值,但应该执行IO!这使得行为相当不可预测,也使得类型系统不那么“纯粹”,因为所有函数都将采用a和IOa值。
输入IO Monad。
要将动作串在一起,只需展平嵌套的动作。要将函数应用于IO操作的输出,IO a类型中的a,只需使用(>>=)。
例如,输出输入的行(输出行是一个生成IO操作的函数,匹配右参数>>=):
getLine >>= putStrLn :: IO ()
-- putStrLn :: String -> IO ()
这可以用do环境更直观地写出来:
do line <- getLine
putStrLn line
本质上,这样的do块:
do x <- a
y <- b
z <- f x y
w <- g z
h x
k <- h z
l k w
…转化为:
a >>= \x ->
b >>= \y ->
f x y >>= \z ->
g z >>= \w ->
h x >>= \_ ->
h z >>= \k ->
l k w
还有m>>=\_->f的>>运算符(当框中的值不需要在框中创建新框时)也可以写成a>>b=a>>=constb(consta b=a)
此外,返回运算符是根据IO直觉建模的-它返回一个具有最小上下文的值,在这种情况下没有IO。由于IO a中的a表示返回的类型,这类似于命令式编程语言中的return(a),但它不会停止操作链!f>>=return>>=g与f>>=g相同。仅当您返回的术语在链中较早创建时才有用-请参见上文。
当然,还有其他Monad,否则它不会被称为Monad,它会被称为“IO控制”之类的东西。
例如,List Monad(Monad[])通过串联变平-使(>>=)运算符对列表的所有元素执行函数。这可以被视为“不确定性”,其中列表是许多可能的值,而Monad框架正在进行所有可能的组合。
例如(GHCi):
Prelude> [1, 2, 3] >>= replicate 3 -- Simple binding
[1, 1, 1, 2, 2, 2, 3, 3, 3]
Prelude> concat (map (replicate 3) [1, 2, 3]) -- Same operation, more explicit
[1, 1, 1, 2, 2, 2, 3, 3, 3]
Prelude> [1, 2, 3] >> "uq"
"uququq"
Prelude> return 2 :: [Int]
[2]
Prelude> join [[1, 2], [3, 4]]
[1, 2, 3, 4]
因为:
join a = concat a
a >>= f = join (fmap f a)
return a = [a] -- or "= (:[])"
如果出现这种情况,“也许莫纳德”只会将所有结果作废为“无”。也就是说,绑定自动检查函数(a>>=f)是否返回或值(a>>>=f)是否为Nothing,然后也返回Nothing。
join Nothing = Nothing
join (Just Nothing) = Nothing
join (Just x) = x
a >>= f = join (fmap f a)
或者更明确地说:
Nothing >>= _ = Nothing
(Just x) >>= f = f x
State Monad用于同时修改某些共享状态-s->(a,s)的函数,因此>>=的参数为:a->s->(a,s)。这个名称有点用词不当,因为State实际上是用于状态修改功能,而不是用于状态——状态本身确实没有有趣的财产,它只是被改变了。
例如:
pop :: [a] -> (a , [a])
pop (h:t) = (h, t)
sPop = state pop -- The module for State exports no State constructor,
-- only a state function
push :: a -> [a] -> ((), [a])
push x l = ((), x : l)
sPush = state push
swap = do a <- sPop
b <- sPop
sPush a
sPush b
get2 = do a <- sPop
b <- sPop
return (a, b)
getswapped = do swap
get2
那么:
Main*> runState swap [1, 2, 3]
((), [2, 1, 3])
Main*> runState get2 [1, 2, 3]
((1, 2), [1, 2, 3]
Main*> runState (swap >> get2) [1, 2, 3]
((2, 1), [2, 1, 3])
Main*> runState getswapped [1, 2, 3]
((2, 1), [2, 1, 3])
也:
Prelude> runState (return 0) 1
(0, 1)
除了上面出色的答案之外,让我为您提供以下文章的链接(由Patrick Thomson撰写),该文章通过将概念与JavaScript库jQuery(及其使用“方法链接”来操作DOM的方式)相关联来解释monads:jQuery是Monad
jQuery文档本身并没有提到术语“monad”,而是谈到了可能更熟悉的“构建器模式”。这并不能改变一个事实,那就是你有一个合适的monad,也许你甚至没有意识到它。
另一种尝试是解释monad,只使用Python列表和map函数。我完全接受这不是一个完整的解释,但我希望它能触及核心概念。
我从Monads上的funfunfunction视频和Learn You A Haskell章节“为了几个Monads更多”中得到了这一点的基础。我强烈推荐观看funfunfunction视频。
最简单的是,Monad是具有map和flatMap函数(在Haskell中绑定)的对象。有一些额外的必需财产,但这些是核心属性。
flatMap“展平”map的输出,对于列表,这只是连接列表的值,例如。
concat([[1], [4], [9]]) = [1, 4, 9]
因此,在Python中,我们基本上可以通过以下两个函数实现Monad:
def flatMap(func, lst):
return concat(map(func, lst))
def concat(lst):
return sum(lst, [])
func是任何接受值并返回列表的函数。
lambda x: [x*x]
解释
为了清楚起见,我通过一个简单的函数在Python中创建了concat函数,该函数将列表相加,即[]+[1]+[4]+[9]=[1,4,9](Haskell有一个原生的concat方法)。
我假设你知道地图功能是什么,例如:
>>> list(map(lambda x: [x*x], [1,2,3]))
[[1], [4], [9]]
展平是Monad的关键概念,对于每个作为Monad的对象,这种展平允许您获得Monad中包裹的值。
现在我们可以呼叫:
>>> flatMap(lambda x: [x*x], [1,2,3])
[1, 4, 9]
这个lambda取一个值x并将其放入一个列表中。monad适用于从值到monad类型的任何函数,所以在本例中是列表。
这是你的monad定义。
我认为为什么它们有用的问题已经在其他问题中得到了回答。
更多说明
其他不是列表的例子有JavaScript Promise,它有then方法,JavaScript Streams有flatMap方法。
因此Promise和Streams使用了一个稍微不同的函数,它将Stream或Promise展平,并从内部返回值。
Haskell列表monad具有以下定义:
instance Monad [] where
return x = [x]
xs >>= f = concat (map f xs)
fail _ = []
即有三个函数return(不要与大多数其他语言中的return混淆)、>>=(flatMap)和fail。
希望您能看到以下两者之间的相似之处:
xs >>= f = concat (map f xs)
and:
def flatMap(f, xs):
return concat(map(f, xs))
我也在努力理解单子。这是我的版本:
Monad是关于对重复的事物进行抽象的。首先,monad本身是一个类型化接口(像抽象泛型类),它有两个函数:bind和return,它们定义了签名。然后,我们可以基于抽象的monad创建具体的monad,当然还有绑定和返回的具体实现。此外,绑定和返回必须满足几个不变量,以便可以组合/链接具体的单体。
当我们有接口、类型、类和其他工具来创建抽象时,为什么要创建monad概念?因为monad提供了更多:它们以一种能够在没有任何样板的情况下合成数据的方式强制重新思考问题。