最近简要介绍了Haskell,对于monad本质上是什么,简单,简洁,实用的解释是什么?
我发现我遇到的大多数解释都是相当难以接近的,缺乏实际细节.
第一:如果你不是数学家,monad这个词有点空洞.另一个术语是计算构建器,它更具描述性,它们实际上是有用的.
你问一些实际的例子:
示例1:列表理解:
[x*2 | x<-[1..10], odd x]
此表达式返回1到10范围内所有奇数的双精度数.非常有用!
事实证明,这实际上只是List monad中某些操作的语法糖.相同的列表理解可以写成:
do x <- [1..10] guard (odd x) return (x * 2)
甚至:
[1..10] >>= (\x -> guard (odd x) >> return (x*2))
示例2:输入/输出:
do putStrLn "What is your name?" name <- getLine putStrLn ("Welcome, " ++ name ++ "!")
两个示例都使用monad,AKA计算构建器.共同的主题是monad 链以一些特定的,有用的方式运作.在列表推导中,操作被链接,使得如果操作返回列表,则对列表中的每个项执行以下操作.另一方面,IO monad按顺序执行操作,但传递"隐藏变量",表示"世界状态",这允许我们以纯函数方式编写I/O代码.
事实证明链接操作的模式非常有用,并且在Haskell中用于许多不同的事情.
另一个例子是例外:使用Error
monad,操作被链接,使得它们按顺序执行,除非抛出错误,在这种情况下,链的其余部分被放弃.
list-comprehension语法和do-notation都是使用>>=
运算符链接操作的语法糖.monad基本上只是一种支持>>=
运营商的类型.
示例3:解析器
这是一个非常简单的解析器,它解析带引号的字符串或数字:
parseExpr = parseString <|> parseNumber parseString = do char '"' x <- many (noneOf "\"") char '"' return (StringValue x) parseNumber = do num <- many1 digit return (NumberValue (read num))
这些操作char
,digit
等等都非常简单.它们匹配或不匹配.神奇的是管理控制流程的monad:操作按顺序执行,直到匹配失败,在这种情况下,monad回溯到最新状态<|>
并尝试下一个选项.同样,一种使用一些额外的,有用的语义来链接操作的方法.
例4:异步编程
上面的例子在Haskell中,但事实证明F#也支持monad.这个例子是从Don Syme偷来的:
let AsyncHttp(url:string) = async { let req = WebRequest.Create(url) let! rsp = req.GetResponseAsync() use stream = rsp.GetResponseStream() use reader = new System.IO.StreamReader(stream) return reader.ReadToEnd() }
此方法获取网页.妙语是使用GetResponseAsync
- 它实际上等待单独线程上的响应,而主线程从函数返回.当收到响应时,最后三行在生成的线程上执行.
在大多数其他语言中,您必须为处理响应的行显式创建单独的函数.该async
单子能够"分裂"自身块和推迟后半期的执行.(async {}
语法表示块中的控制流由async
monad 定义.)
他们如何工作
那么monad怎么能做所有这些奇特的控制流程呢?在do-block(或在F#中调用的计算表达式)中实际发生的是每个操作(基本上每一行)都包含在一个单独的匿名函数中.然后使用bind
运算符(>>=
在Haskell中拼写)组合这些函数.由于bind
操作组合了函数,它可以按照它认为合适的方式执行它们:顺序,多次,反向,丢弃一些,在感觉到它时在单独的线程上执行一些等等.
例如,这是示例2中IO代码的扩展版本:
putStrLn "What is your name?" >>= (\_ -> getLine) >>= (\name -> putStrLn ("Welcome, " ++ name ++ "!"))
这是更加丑陋的,但实际发生的事情也更加明显.该>>=
操作是神奇的成分:它需要一个值(左侧)和具有功能(右侧)相结合的,以产生新的价值.然后,下一个>>=
运算符将获取该新值,并再次与函数组合以生成新值.>>=
可以被视为一个迷你评估者.
请注意,>>=
对于不同的类型会重载,因此每个monad都有自己的实现>>=
.(链中的所有操作都必须是同一个monad的类型,否则>>=
操作符将无法工作.)
最简单的实现>>=
只是获取左边的值并将其应用于右边的函数并返回结果,但如前所述,使整个模式有用的是当monad的实现中有一些额外的事情发生时>>=
.
值如何从一个操作传递到下一个操作还有一些额外的聪明,但这需要对Haskell类型系统进行更深入的解释.
加起来
在Haskell术语中,monad是一个参数化类型,它是Monad类型的一个实例,它>>=
与一些其他运算符一起定义.在外行人看来,monad只是一种>>=
定义操作的类型.
本身>>=
只是链接函数的一种繁琐的方式,但是由于存在隐藏"管道"的记号,monadic操作变成了一个非常好的和有用的抽象,在语言的许多地方很有用,并且很有用用语言创建自己的迷你语言.
为什么单子难?
对于许多Haskell学习者来说,monad是他们像砖墙一样的障碍.并不是monad本身很复杂,而是实现依赖于许多其他高级Haskell功能,如参数化类型,类型类等.问题是Haskell I/O基于monad,I/O可能是你在学习新语言时想要理解的第一件事 - 毕竟,创建不生成任何语言的程序并不是很有趣输出.我没有立即解决这个鸡和蛋的问题,除非像对待"魔法发生在这里"之类的I/O,直到你有足够的语言其他部分经验.抱歉.
关于monad的优秀博客:http: //adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html
解释"什么是monad"有点像说"什么是数字?" 我们一直使用数字.但想象你遇到了一个对数字一无所知的人.如何赫克你能解释的数字是什么?你怎么会开始描述为什么这可能有用呢?
什么是monad?简短的回答:这是一种将操作链接在一起的特定方式.
实质上,您正在编写执行步骤并使用"绑定功能"将它们链接在一起.(在Haskell中,它被命名>>=
.)您可以自己编写对bind操作符的调用,或者您可以使用语法sugar,使编译器为您插入这些函数调用.但无论哪种方式,每个步骤都通过调用此绑定函数来分隔.
所以bind函数就像一个分号; 它分离了一个过程中的步骤.绑定功能的作用是获取上一步的输出,并将其输入下一步.
这听起来不太难,对吧?但是不止一种单子.为什么?怎么样?
好吧,绑定功能可以从一步获取结果,并将其提供给下一步.但是,如果这是"所有"monad所做的......那实际上并不是非常有用.这一点很重要:每个有用的 monad 除了成为monad 之外还会做其他事情.每一个有用的单子都有"特殊的力量",这使它独一无二.
(没有什么特别之处的monad 被称为"身份monad".与身份功能相似,这听起来像一个完全没有意义的事情,但结果却不是......但那是另一个故事™.)
基本上,每个monad都有自己的bind函数实现.您可以编写一个绑定函数,以便在执行步骤之间进行连接.例如:
如果每个步骤都返回一个成功/失败指示符,则只有在前一个步骤成功的情况下,才能让绑定执行下一步.这样,失败的步骤会"自动"中止整个序列,而不需要您进行任何条件测试.(The Failure Monad.)
扩展这个想法,你可以实现"例外".(错误Monad或Exception Monad.)因为您自己定义它们而不是语言功能,所以您可以定义它们的工作方式.(例如,您可能希望忽略前两个异常,并且仅在抛出第三个异常时中止.)
您可以使每个步骤返回多个结果,并使绑定函数循环遍历它们,将每个步骤提供给下一步.这样,在处理多个结果时,您不必在整个地方继续编写循环.绑定功能"自动"为您完成所有这些.(名单Monad.)
除了将"结果"从一个步骤传递到另一个步骤之外,您还可以让bind函数传递额外的数据.此数据现在不会显示在您的源代码中,但您仍然可以从任何地方访问它,而无需手动将其传递给每个函数.(读者Monad.)
您可以将其设置为可以替换"额外数据".这允许您模拟破坏性更新,而无需实际进行破坏性更新.(国家莫纳德及其堂兄作家莫纳德.)
因为您只是在模拟破坏性更新,所以您可以通过真正的破坏性更新轻松完成这些操作.例如,您可以撤消上次更新,或恢复为旧版本.
您可以创建一个可以暂停计算的monad ,这样您就可以暂停程序,进入并修改内部状态数据,然后恢复它.
您可以将"continuation"实现为monad.这可以让你打破人们的思想!
monad可以实现所有这些以及更多.当然,如果没有 monad ,所有这一切也是完全可能的.使用monads 非常容易.
实际上,与莫纳德的共同理解相反,他们与国家无关.Monads只是一种包装东西的方法,并提供了对包裹的东西进行操作的方法,而无需展开它.
例如,您可以在Haskell中创建一个类型来包装另一个类型:
data Wrapped a = Wrap a
包装我们定义的东西
return :: a -> Wrapped a return x = Wrap x
要在不解包的情况下执行操作,假设您有一个函数f :: a -> b
,那么您可以执行此操作来解除该函数以对包装值执行操作:
fmap :: (a -> b) -> (Wrapped a -> Wrapped b) fmap f (Wrap x) = Wrap (f x)
这就是所有需要了解的内容.然而,事实证明,有一个更通用的功能来完成这个提升,这是bind
:
bind :: (a -> Wrapped b) -> (Wrapped a -> Wrapped b) bind f (Wrap x) = f x
bind
可以做多一点fmap
,但反之亦然.实际上,fmap
只能用bind
和来定义return
.因此,在定义monad时...你给它的类型(这里是Wrapped a
),然后说出它return
和bind
操作是如何工作的.
很酷的是,事实证明这是一种普遍的模式,它会在整个地方弹出,以纯粹的方式封装状态只是其中之一.
有关如何使用monad来引入函数依赖关系并因此控制评估顺序的好文章,就像它在Haskell的IO monad中使用一样,请查看IO Inside.
至于理解monad,不要太担心它.阅读他们您感兴趣的内容,如果您不理解,请不要担心.然后,只需要像Haskell这样的语言潜水就可以了.Monads是通过练习理解涓涓细流到你的大脑的其中一件事,有一天你突然意识到你理解它们.
但是,你可能已经发明了Monads!
sigfpe说:
但是所有这些都将monad引入了需要解释的深奥的东西.但我想说的是,它们根本不是深奥的.事实上,面对函数式编程中的各种问题,你不可避免地会遇到某些解决方案,所有解决方案都是monad的例子.事实上,如果你还没有,我希望你现在可以发明它们.然后,这是一个小步骤,注意到所有这些解决方案实际上都是伪装的相同解决方案.阅读本文之后,你可能会更好地理解monad上的其他文档,因为你会认识到你所看到的所有你已经发明的东西.
monad试图解决的许多问题都与副作用问题有关.所以我们将从他们开始.(注意monads让你做的不仅仅是处理副作用,特别是许多类型的容器对象可以被视为monad.对monad的一些介绍发现很难调和monad的这两种不同用法并且只关注一个或者另一个.)
在诸如C++这样的命令式编程语言中,函数的行为与数学函数完全不同.例如,假设我们有一个C++函数,它接受一个浮点参数并返回一个浮点结果.从表面上看,它似乎有点像数学函数映射实数到实数,但C++函数可以做的不仅仅是返回一个取决于其参数的数字.它可以读取和写入全局变量的值,也可以将输出写入屏幕并接收来自用户的输入.但是,在纯函数式语言中,函数只能读取其参数中提供给它的内容,并且它对世界产生影响的唯一方法是通过它返回的值.
monad是一种具有两个操作的数据类型:>>=
(aka bind
)和return
(aka unit
).return
获取任意值并使用它创建monad的实例.>>=
获取monad的实例并在其上映射函数.(您已经可以看到monad是一种奇怪的数据类型,因为在大多数编程语言中,您无法编写一个采用任意值并从中创建类型的函数.Monads使用一种参数多态.)
在Haskell表示法中,编写了monad接口
class Monad m where return :: a -> m a (>>=) :: forall a b . m a -> (a -> m b) -> m b
这些操作都应该遵守一定的"规律",但是这不是很重要了:"法律"只是编纂操作的方式明智的实现应该表现(基本上,>>=
并return
应该同意的价值观是如何被转化成单子实例和这>>=
是关联的).
Monads不只是关于状态和I/O:它们抽象了一种常见的计算模式,包括使用状态,I/O,异常和非确定性.可能最容易理解的monad是列表和选项类型:
instance Monad [ ] where [] >>= k = [] (x:xs) >>= k = k x ++ (xs >>= k) return x = [x] instance Monad Maybe where Just x >>= k = k x Nothing >>= k = Nothing return x = Just x
其中[]
和:
是列表构造函数,++
是连接运算符,Just
并且Nothing
是Maybe
构造函数.这两个monad都在各自的数据类型上封装了常见且有用的计算模式(请注意,这两种模式都与副作用或I/O无关).
你真的必须编写一些非平凡的Haskell代码,以了解monad的含义以及它们有用的原因.
你应该先了解一个仿函数是什么.在此之前,了解更高阶的函数.
甲高阶函数是一个简单的函数,它的功能作为一个参数.
甲算符是任何类型的构造T
为其中存在一个高阶函数,调用它map
,该变换类型的函数a -> b
(给定任何两种类型的a
和b
)成一个函数T a -> T b
.此map
函数还必须遵守身份和组合的规律,以便以下表达式对所有人p
和q
(Haskell表示法)都返回true :
map id = id map (p . q) = map p . map q
例如,如果一个类型构造List
函数配备了(a -> b) -> List a -> List b
符合上述规则的类型函数,则它被称为仿函数.唯一实际的实施是显而易见的.结果List a -> List b
函数遍历给定列表,(a -> b)
为每个元素调用函数,并返回结果列表.
一个单子本质上只是一个仿函数T
有两个额外的方法,join
,类型T (T a) -> T a
,和unit
(有时被称为return
,fork
或pure
类型)a -> T a
.对于Haskell中的列表:
join :: [[a]] -> [a] pure :: a -> [a]
为什么这有用?例如,因为您可以map
在具有返回列表的函数的列表上.Join
获取结果列表并将它们连接起来.List
是monad,因为这是可能的.
你可以写,做了功能map
,那么join
.此函数被称为bind
,或flatMap
,或(>>=)
,或(=<<)
.这通常是在Haskell中给出monad实例的方式.
monad必须满足某些定律,即join
必须是关联的.这意味着如果你有一个x
类型的值,[[[a]]]
那么join (join x)
应该相等join (map join x)
.而且pure
必须是一个身份join
,使得join (pure x) == x
.
[免责声明:我仍然试图完全修改monad.以下是我到目前为止所理解的内容.如果这是错的,希望知识渊博的人会叫我在地毯上.]
阿纳写道:
Monads只是一种包装东西的方法,并提供了对包裹的东西进行操作的方法,而无需展开它.
这正是它.这个想法是这样的:
你需要一些价值并用一些额外的信息包装它.就像值是某种类型(例如,整数或字符串)一样,附加信息也是某种类型.
例如,额外的信息可能是a Maybe
或a IO
.
然后,您有一些运算符,允许您在携带附加信息的同时对包装数据进行操作.这些运算符使用附加信息来决定如何更改包装值上的操作行为.
例如,a Maybe Int
可以是a Just Int
或Nothing
.现在,如果你添加一个Maybe Int
到Maybe Int
,运营商将检查,看看他们都是Just Int
里面装的,如果是的话,那里展开Int
S,通过他们的加法运算,形成的再包装Int
到一个新的Just Int
(这是一个有效Maybe Int
),从而返回一个Maybe Int
.但是如果其中一个是Nothing
内部的,那么这个操作符将立即返回Nothing
,这也是有效的Maybe Int
.这样,您可以假装您的Maybe Int
s只是正常数字并对它们进行常规数学运算.如果你得到一个Nothing
,你的方程式仍将产生正确的结果 - 你无需在Nothing
任何地方乱丢垃圾.
但这个例子正是发生了什么Maybe
.如果额外信息是a IO
,那么为IO
s 定义的特殊运算符将被调用,并且在执行添加之前它可以做一些完全不同的事情.(好吧,将两个IO Int
s加在一起可能是荒谬的 - 我还不确定.)(另外,如果你注意这个Maybe
例子,你已经注意到"用额外的东西包装一个值"并不总是正确的.但它很难确切,准确,准确,不可理解.)
基本上,"monad"大致意味着"模式".但是,您现在拥有一个语言结构 - 语法和全部 - 而不是一本充满非正式解释和特别命名的模式的书,它允许您将新模式声明为程序中的事物.(这里的不精确是所有的模式都必须遵循一种特定的形式,所以monad并不像模式那样通用.但我认为这是大多数人都知道和理解的最接近的术语.)
这就是人们发现monad如此混乱的原因:因为它们是如此通用的概念.要问是什么使monad成为同样含糊不清的问题是什么使某事成为一种模式.
但是想一想在语言中使用语法支持对模式概念的影响:你不必阅读" 四人帮"一书并记住特定模式的构造,你只需编写一个在不可知的情况下实现这种模式的代码,一般的方式,然后你就完成了!然后,您可以重复使用此模式,例如Visitor或Strategy或Façade等等,只需通过使用它来装饰代码中的操作,而无需反复重复实现它!
所以这就是为什么理解 monad的人发现它们如此有用:它不是一些象牙塔概念,知识分子自豪于理解(好吧,当然,teehee),但实际上使代码更简单.
经过多次努力,我想我终于明白了monad.在重读了我自己对压倒性的最高投票答案的冗长批评后,我将提供这种解释.
理解monad需要回答三个问题:
你为什么需要monad?
什么是monad?
monad是如何实现的?
正如我在原始评论中所指出的那样,太多的monad解释被问到第3个问题,没有,并且在真正充分覆盖问题2或问题1之前.
你为什么需要monad?
像Haskell这样的纯函数式语言与命令式语言(如C或Java)的不同之处在于,纯函数式程序不一定按特定顺序执行,一次一步.Haskell程序更类似于数学函数,您可以在其中解决任意数量的潜在订单中的"等式".这带来了许多好处,其中之一是它消除了某些类型的错误的可能性,特别是那些与"状态"有关的错误.
但是,使用这种编程方式存在一些不易解决的问题.有些东西,比如控制台编程和文件i/o,需要按特定顺序发生,或者需要维护状态.解决此问题的一种方法是创建一种表示计算状态的对象,以及将状态对象作为输入并返回新的已修改状态对象的一系列函数.
因此,让我们创建一个假设的"状态"值,它表示控制台屏幕的状态.究竟如何构造这个值并不重要,但是假设它是一个字节长度为ascii的字符数组,表示屏幕上当前可见的内容,以及一个数组,表示用户输入的最后一行输入,以伪代码形式显示.我们已经定义了一些处理控制台状态,修改它并返回新控制台状态的函数.
consolestate MyConsole = new consolestate;
所以要进行控制台编程,但是以纯函数方式,你需要在彼此内部嵌套很多函数调用.
consolestate FinalConsole = print(input(print(myconsole, "Hello, what's your name?")),"hello, %inputbuffer%!");
以这种方式编程保持"纯粹"的功能样式,同时强制对控制台的更改以特定顺序发生.但是,我们可能希望不仅仅像上面的例子那样只做一些操作.以这种方式嵌套功能将开始变得笨拙.我们想要的是,代码与上面的内容基本相同,但写得更像这样:
consolestate FinalConsole = myconsole: print("Hello, what's your name?"): input(): print("hello, %inputbuffer%!");
这确实是一种更方便的编写方式.我们怎么做呢?
什么是monad?
一旦你有一个consolestate
你定义的类型(例如)以及专门设计用于该类型的一系列函数,你可以通过定义像:
(bind)这样的运算符将这些东西的整个包变成"monad" feed返回左侧的值,右侧的函数参数,以及lift
将正常函数转换为与特定类型的绑定运算符一起使用的函数.
monad是如何实现的?
看到其他答案,似乎可以自由地进入细节.
monad实际上是一种"类型操作符".它会做三件事.首先,它将"包装"(或以其他方式转换)一种类型的值到另一种类型(通常称为"monadic类型").其次,它将使基础类型上的所有操作(或函数)在monadic类型上可用.最后,它将支持将自己与另一个monad组合以生成复合monad.
"may monad"本质上相当于Visual Basic/C#中的"可空类型".它采用非可空类型"T"并将其转换为"Nullable
副作用以类似方式表示.创建一个结构,其中包含副作用的描述以及函数的返回值.然后,"提升"操作在函数之间传递值时复制副作用.
它们被称为"monad",而不是"类型操作符"更容易掌握的名称,原因如下:
Monads对他们可以做的事情有限制(详情请参阅definiton).
这些限制以及涉及三个操作的事实符合类别理论中称为monad的结构,这是数学的一个模糊分支.
它们是由"纯粹"功能语言的支持者设计的
纯函数式语言的支持者,比如模糊的数学分支
因为数学是模糊的,并且monad与特定的编程风格相关联,所以人们倾向于使用monad这个词作为一种秘密握手.因此,没有人愿意投资一个更好的名字.
我主要是为我写这篇文章,但我希望其他人觉得有用:)
我相信这种解释更为正确.但是,我认为这种治疗仍然很有价值,并会考虑在以后加入它.可以这么说,传统的函数组合处理函数普通值,Monads是关于组成函数值的函数(高阶函数).当你正在处理高阶函数(函数接受或返回功能),该组合物必须定制或定制的,以便在该组合物进行评估,以评估操作数.此评估过程可能是异国情调,例如收集异步过程的结果.尽管如此,这种剪裁可以遵循一种模式.该模式的一个版本称为Monad,并且遵循非常多的代数.特别地,对于以下内容,这样的高阶函数将被视为表达式中的数学运算符,其接受操作数和其他部分应用的运算符,因此函数1 + 2*,3 /和7+在bind
...中
Monads解决了一个问题,它也在算术中显示为零除,compose (>>)
.具体而言,涉及除法的计算必须检测或允许(x -> y) >> (y -> z)
异常.这个要求使得在一般情况下编码这样的表达式变得混乱.
Monadic的解决方案是y
通过以下方式来实现
展开Mb
式,包括(is_OK, b)
作为是不是正规号码的特定值:y
,bool
,或bool * float
.让我们称这个新的数字类型,bool
.
提供"提升"或将现有包装(Ma -> Mb) >> (Mb -> Mc)
成compose
类型的功能("包装"的想法是内容is_OK
或值可以"解包"而不会丢失信息)
提供将"提升"或包装现有操作员的功能false
转换为可在其上运行的版本bind (>>=)
.这样一个"提升"的操作员可能只做以下事情:
展开提供的composition
操作数并将其包含的compose
运算符应用于它们然后将Bind
结果"提升" 为aM
Ma
在评估期间检测操作数或异常,并通过进一步评估,生成一个a
值作为断言(bind
)的结果.但是,采取什么行动取决于程序员.通常,这些包装函数是编写Monad的许多功能的地方.monadic状态信息保存在包装类型本身内,包装函数检查它,并且根据函数式编程不变性方法,构造一个新的monadic值.在这种情况下M
,这种一元状态信息将描述是否存在a
或实际bind
存在.
因此,Monad是一个扩展类型,它具有将原始类型"包装"到此扩展版本中的函数,以及另一个包装原始操作符的函数,以便它们可以处理这种新的扩展类型.(Monads可能是仿制药或类型参数的动机.)
事实证明,a
Monad处理可以广泛应用于可以从类型扩展中受益以简化编码的情况,而不仅仅是平滑处理(或无限制).事实上,这种适用性似乎很广泛.
例如M
,字面意思是表示宇宙的类型.目的是要认识到原型(a -> Mb) >>= (b -> Mc)
程序返回的值不是由结果类型Mb >>= (b -> Mc)
及其值"Hello World!" 完全描述的.实际上,这样的结果还包括对诸如控制台之类的设备的硬件和存储器状态的修改.例如,执行后控制台现在显示其他文本,光标位于新行上,依此类推.shell and content
如果你愿意的话,这只是对这种外在影响或副作用的明确认识.
何必?
Monads允许设计严格的无状态算法并记录状态完整的算法.国家机器很复杂.例如,仅具有10比特的机器可以处于2 ^ 10个可能状态.消除多余的复杂性是功能语言的理想选择.
变量保持状态.消除"变量"应该只是填充.纯功能程序不处理变量,只处理值(尽管在Haskell文档中使用了术语'变量'),而是根据需要使用标签或符号或名称作为这些值.因此,与纯函数语言中的变量最接近的是函数接收的参数,因为它们在每次调用时接受新值.(标签是指一个值,而变量指的是一个值被保留的位置.因此,你可以修改一个变量的内容,但标签就是内容本身.最终给予一个苹果比一个包更好可能还有一个苹果.)
缺少变量是纯函数式语言使用递归而不是循环迭代的原因.递增计数器的行为涉及使用变量增加的变量以及变更时的所有不确定性,测试时的变化,应该是什么值以及何时更新,以及当多个线程可能访问该变量时的复杂性相同的变量.
不过,那又怎样?
如果没有状态,函数必须成为它的结果的声明或定义,而不是将某些潜在的状态纳入结果.本质上,函数表达式bind
比M
Here 的命令式表达式更简单,bind
不会修改a -> Mb
但会创建新值.incFun甚至可以被表达式中的定义替换为Mb
成为0
.另一方面,monad
修改状态a
.无论这种修改意味着什么,Ma
都可能不清楚,并且除了任何并发问题之外,最终无法在不执行程序的情况下确定.
随着时间的推移,这种复杂性在认知上会变得昂贵(2 ^ N).相反,运算符a -> b
不能修改,a -> Mb
而必须构造一个新值,其结果受限于并完全由值bind
和M
定义决定bind
.特别是,避免了2 ^ N复杂性爆炸.此外,为了强调并发性,Associativity
不同的是bind
,不能在没有注意事项共享的情况下同时调用,因为它会被每次调用修改.
为什么称它为Monad?
Monad的特征在于来自代数群理论的称为Monoid的数学结构.话虽如此,所有这意味着Monoid具有以下三个属性:
有一个二元运算符,bind
这样Associativity
就binding
属于某种类型f
.例如1÷2 = 0.5,其中1,2和0.5都是类型g
.关闭
有一个标识元素,Ma
与二进制运算符相关联,不执行任何操作bind
.例如,数字运算符+和数字0,在4 + 0 = 0 + 4 = 4中.标识
评估"细分"的顺序无关紧要:Ma
.例如,数字运算符+,in f
.但请注意,术语的顺序不得更改.关联性
属性三(关联性)允许通过将它们描绘成段并且独立地评估每个段(例如并行)来评估任意长度的表达.例如,g
可以分段为bind
.compose (>>)
然后可以收集单独的结果并进行类似的进一步评估.像这样支持分段是确保正确的并发性和分布式评估的关键技术,如Google的分布式搜索算法(la map/reduce)所使用的.
属性二(Identity)允许以各种方式更容易地构造表达式,尽管它可能并不完全明显; 然而,就像早期计数系统显然不需要零一样,它可以作为空的概念用于包装空值.请注意,在类型,(x -> y) >> (y -> z)
,y
是不是空值,而是Mb
.具体而言,(is_OK, b)
而y
,因此bool
仍在身份bool * float
,其中bool
是任何(Ma -> Mb) >> (Mb -> Mc)
.
最后,有一个原因我们不再使用罗马数字...没有扩展的零或分数,无理数,负数,虚数,... ...是的,似乎我们的数字系统可以被认为是monad.
(另请参阅什么是monad的答案?)
Monads的一个好动机是sigfpe(Dan Piponi)你可以发明Monad!(也许你已经拥有).还有其他的单子教程很多,其中有不少misguidedly尝试使用各种类比于"深入浅出"来解释单子:这是单子教程谬误 ; 避免他们.
正如DR MacIver所说,告诉我们为什么你的语言很糟糕:
所以,我讨厌Haskell的事情:
让我们从明显的开始.Monad教程.不,不是单子.特别是教程.他们是无尽的,夸张的,亲爱的上帝,他们是乏味的.此外,我从未见过任何令人信服的证据证明他们确实有所帮助.阅读类定义,编写一些代码,克服可怕的名字.
你说你了解Maybe monad?好的,你在路上.刚开始使用其他monad,迟早你会明白monad是什么.
[如果你是数学为主,你可能想忽略几十个教程和学习的定义,或者按照范畴论讲座 :)定义的主要部分是一个单子中号涉及到一个"类型构造",它定义了每个现有类型"T"是一种新型"MT",以及在"常规"类型和"M"类型之间来回切换的一些方法.
此外,令人惊讶的是,最好的引见一个单子实际上是早期的学术论文引入单子,菲利普·沃德勒的一个单子函数式编程.它实际上具有实用的,非平凡的激励示例,与许多人工教程不同.
Monads用于控制抽象数据类型对数据的流量.
换句话说,许多开发人员对集合,列表,字典(或哈希,或地图)和树木的想法感到满意.在这些数据类型中有许多特殊情况(例如InsertionOrderPreservingIdentityHashMap).
然而,当面对程序"流"时,许多开发人员还没有接触到比if,switch/case,do,while,goto(grr)和(可能)闭包更多的构造.
因此,monad只是一个控制流构造.替换monad的更好的短语是"控制类型".
因此,monad具有用于控制逻辑或语句或函数的插槽 - 数据结构中的等价物可以说某些数据结构允许您添加数据并将其删除.
例如,"if"monad:
if( clause ) then block
最简单的有两个插槽 - 一个子句和一个块.该if
单子通常是建立评估条款的结果,如果不是假的,评估该块.许多开发人员在学习'if'时并没有被介绍给monad,并且没有必要理解monad来编写有效的逻辑.
Monads可能变得更加复杂,就像数据结构变得更复杂一样,但是monad的许多类别可能具有相似的语义,但实现和语法不同.
当然,以可以迭代或遍历数据结构的相同方式,可以评估monad.
编译器可能支持也可能不支持用户定义的monad.Haskell当然可以.Ioke有一些类似的功能,虽然monad这个词没有在语言中使用.
我最喜欢的Monad教程:
http://www.haskell.org/haskellwiki/All_About_Monads
(谷歌搜索"monad教程"中的170,000次点击!)
@Stu:monad的观点是允许你将(通常)顺序语义添加到其他纯代码中; 你甚至可以编写monad(使用Monad Transformers)并获得更有趣和复杂的组合语义,例如解析错误处理,共享状态和日志记录.所有这些都可以在纯代码中实现,monads只允许你将其抽象出来并在模块化库中重复使用(总是很好的编程),并提供方便的语法使其看起来势在必行.
Haskell已经有了运算符重载[1]:它使用类型类的方式与在Java或C#中使用接口的方式非常相似,但Haskell恰好也允许使用+ &&和>等非字母数字标记作为中缀标识符.如果你的意思是"超载分号",那么只有运算符重载才能看到它[2].这听起来像是黑魔法并且要求"超出分号"的麻烦(图片很有进取心的Perl黑客得到了这个想法的风)但重点是没有monad 没有分号,因为纯功能代码不需要或允许显式排序.
这一切听起来都比它需要的复杂得多.sigfpe的文章非常酷,但是使用Haskell来解释它,这种方法无法打破鸡和蛋的问题,理解Haskell来修复Monads并理解Monads以克服Haskell.
[1]这是monad的一个独立问题,但monad使用Haskell的运算符重载功能.
[2]这也是一个过于简单化,因为链接monadic动作的运算符是>> =(发音为"bind")但是有句法糖("do")允许你使用大括号和分号和/或缩进和换行符.
最近,我一直在以不同的方式思考Monads.我一直认为它们是以数学方式抽象出执行顺序,这使得新的多态性成为可能.
如果您正在使用命令式语言,并且按顺序编写了一些表达式,则代码ALWAYS将按此顺序运行.
在简单的情况下,当你使用monad时,感觉是一样的 - 你定义了一个按顺序发生的表达式列表.除此之外,根据您使用的monad,您的代码可能按顺序运行(如在IO monad中),同时并行运行多个项目(如List monad),它可能会中途停止(如在Maybe monad中) ,它可能会暂停一段时间以便稍后恢复(比如在Resumption monad中),它可能会从头开始倒回并开始(就像在一个Transaction monad中),或者它可能会在中途回放以尝试其他选项(如在Logic monad中) .
并且因为monad是多态的,所以可以根据您的需要在不同的monad中运行相同的代码.
此外,在某些情况下,可以将monad组合在一起(使用monad变换器)以同时获得多个功能.
我仍然是monads的新手,但我想我会分享一个我觉得非常好读的链接(带图片!!):http: //www.matusiak.eu/numerodix/blog/2012/3/11/ monads-for-the-layman / (没有隶属关系)
基本上,我从文章中得到的温暖和模糊概念是monad基本上是适配器的概念,它允许不同的函数以可组合的方式工作,即能够串起多个函数并混合和匹配它们而不必担心不一致的返回类型等.因此,当我们尝试制作这些适配器时,BIND功能负责将苹果与苹果和橙子与橙子保持在一起.和升降功能是负责以"低级别"功能和"升级"他们与BIND功能工作,可组合为好.
我希望我做对了,更重要的是,希望这篇文章对monad有一个有效的观点.如果不出意外,这篇文章有助于激发我对学习Monad的兴趣.
除了上面的优秀答案之外,让我为您提供以下文章(Patrick Thomson)的链接,该文章通过将概念与JavaScript库jQuery(以及使用"方法链接"来操作DOM)的方式相关联来解释monads : jQuery是Monad
在jQuery文档本身并不指"单子"一词,但谈到了"构建者模式",这可能是比较熟悉的.这并没有改变你有一个合适的monad甚至没有意识到它的事实.
Monads不是隐喻,而是一种从普通模式中产生的实用抽象,正如Daniel Spiewak解释的那样.
monad是一种将计算结合在一起的方式,它们共享一个共同的上下文.这就像建立一个管道网络.构建网络时,没有数据流过它.但是当我用'bind'和'return'完成所有位的拼接后,我调用类似的东西runMyMonad monad data
,数据流过管道.
在实践中,monad是函数组合运算符的自定义实现,它负责处理副作用以及不兼容的输入和返回值(用于链接).
如果我正确理解,则IEnumerable是从monad派生的。我想知道对于C#世界中的那些人来说这是否可能是一个有趣的方法?
对于它的价值,这里有一些帮助我的教程的链接(不,我仍然不知道什么是monads)。
http://osteele.com/archives/2007/12/overloading-semicolon
http://spbhug.folding-maps.org/wiki/MonadsEn
http://www.loria.fr/~kow/monads/
在了解有关时,这两件事对我有所帮助:
第8章,"功能解析器",来自Graham Hutton的书" Haskell编程".实际上,这根本没有提到monad,但是如果你可以完成章节并真正理解其中的所有内容,特别是如何评估一系列绑定操作,你就会理解monad的内部结构.期待这需要几次尝试.
All About Monads教程.这给出了几个很好的例子,我不得不说Appendex中的类比我为我工作.
Monoid似乎是确保Monoid和受支持类型上定义的所有操作始终在Monoid内返回支持类型的东西.例如,任何数字+任何数字=数字,没有错误.
除非接受两个小数,然后返回一个小数,它将除以零定义为无穷大的haskell somewhy(这恰好是一个小数)...
在任何情况下,看起来Monads只是一种确保你的操作链以可预测的方式运行的方法,并且声称为Num - > Num的函数,由另一个Num-> Num函数组成,用x调用说,发射导弹.
另一方面,如果我们有一个发射导弹的功能,我们可以用其他功能组成它,这也发射导弹,因为我们的意图很明确 - 我们想发射导弹 - 但它不会尝试出于某些奇怪的原因打印"Hello World".
在Haskell中,main是IO()或IO [()]类型,这种干扰是很奇怪的,我不会讨论它,但这是我认为发生的事情:
如果我有main,我希望它做一系列动作,我运行程序的原因是产生效果 - 通常是IO.因此,我可以在主要链接IO操作,以便 - 做IO,没有别的.
如果我尝试做一些不"返回IO"的事情,那么程序会抱怨链条没有流动,或者基本上"这与我们正在尝试做什么有关 - 一个IO行动",它似乎是强制的程序员保持他们的思路,不偏离并考虑发射导弹,同时创建排序算法 - 不流动.
基本上,Monads似乎是编译器的一个提示,"嘿,你知道这个函数在这里返回一个数字,它实际上并不总是有效,它有时可以生成一个数字,有时甚至没有任何东西,只需保留它心神".知道这一点,如果你试图断言一个monadic动作,monadic动作可能会作为一个编译时异常说"嘿,这实际上不是一个数字,这可以是一个数字,但你不能假设这一点,做一些事情确保流量可以接受." 这在很大程度上阻止了不可预测的程序行为.
看起来monad不是关于纯度,也不是控制,而是关于维护所有行为可预测和定义的类别的标识,或者不编译.当你被期望做某事时你不能做任何事情,如果你不想做任何事情(可见),你就无法做某事.
我想到Monads的最大原因是 - 去看一下Procedural/OOP代码,你会注意到你不知道程序在哪里开始,也没有结束,所有你看到的都是大量的跳跃和大量的数学运算,魔法和导弹.您将无法维护它,如果可以,您将花费大量时间将整个程序包裹在整个程序中,然后才能理解它的任何部分,因为此上下文中的模块化基于相互依赖的"部分"代码,其中代码被优化为尽可能相关以保证效率/相互关系.Monads非常具体,定义明确,并确保程序流程可以分析,并隔离难以分析的部分 - 因为它们本身就是monad.monad似乎是一个"易于理解的单位,可以预测它的完全理解" - 如果你理解"可能"monad,除了"Maybe"之外它没有任何可能做的事情,这看似微不足道,但在大多数非monadic中代码,一个简单的功能"helloworld"可以发射导弹,什么也不做,或破坏宇宙,甚至扭曲时间 - 我们不知道也不保证它是什么.Monad保证它是什么.这是非常强大的.
"现实世界"中的所有东西似乎都是单子,因为它受到明确的可观察法则的约束,可防止混淆.这并不意味着我们必须模仿这个对象的所有操作来创建类,而是我们可以简单地说"一个正方形是一个正方形",只是一个正方形,甚至不是一个矩形,也不是一个圆形,并且"一个正方形有区域它的一个现有尺寸的长度乘以它自己.无论你有什么方形,如果它是2D空间中的正方形,它的面积绝对不能是它的长度平方,它几乎是无足轻重的证明.这非常强大,因为我们不需要做出断言来确保我们的世界就是这样,我们只是利用现实的含义来阻止我们的计划脱离轨道.
我几乎可以肯定是错的,但我认为这可以帮助那里的人,所以希望它有助于某人.
在Scala的上下文中,您会发现以下内容是最简单的定义.基本上flatMap(或bind)是'associative'并且存在一个标识.
trait M[+A] {
def flatMap[B](f: A => M[B]): M[B] // AKA bind
// Pseudo Meta Code
def isValidMonad: Boolean = {
// for every parameter the following holds
def isAssociativeOn[X, Y, Z](x: M[X], f: X => M[Y], g: Y => M[Z]): Boolean =
x.flatMap(f).flatMap(g) == x.flatMap(f(_).flatMap(g))
// for every parameter X and x, there exists an id
// such that the following holds
def isAnIdentity[X](x: M[X], id: X => M[X]): Boolean =
x.flatMap(id) == x
}
}
例如
// These could be any functions
val f: Int => Option[String] = number => if (number == 7) Some("hello") else None
val g: String => Option[Double] = string => Some(3.14)
// Observe these are identical. Since Option is a Monad
// they will always be identical no matter what the functions are
scala> Some(7).flatMap(f).flatMap(g)
res211: Option[Double] = Some(3.14)
scala> Some(7).flatMap(f(_).flatMap(g))
res212: Option[Double] = Some(3.14)
// As Option is a Monad, there exists an identity:
val id: Int => Option[Int] = x => Some(x)
// Observe these are identical
scala> Some(7).flatMap(id)
res213: Option[Int] = Some(7)
scala> Some(7)
res214: Some[Int] = Some(7)
注:严格地说,函数式编程中Monad的定义与类别理论中Monad的定义不同,类别理论的定义是map
和flatten
.虽然它们在某些映射下是等价的.这个演讲非常好:http://www.slideshare.net/samthemonad/monad-presentation-scala-as-a-category
这个答案从一个激励性的例子开始,通过例子,推导出一个monad的例子,并正式定义"monad".
在伪代码中考虑这三个函数:
f() := g( ) := wrap(x) :=
f
获取一对有序的表单
并返回一个有序对.它使第一个项目保持不变并附"called f. "
加到第二个项目.与...相同g
.
您可以组合这些函数并获取原始值,以及显示函数调用顺序的字符串:
f(g(wrap(x))) = f(g()) = f( ) =
您不喜欢这样的事实,f
并g
负责将自己的日志消息附加到以前的日志记录信息.(想象一下为了论证而不是追加字符串,f
并且g
必须在对的第二项上执行复杂的逻辑.在两个或更多个不同的函数中重复那个复杂的逻辑会很痛苦.)
您更喜欢编写更简单的函数:
f(x) :=g(x) := wrap(x) :=
但看看你撰写时会发生什么:
f(g(wrap(x))) = f(g()) = f(< , "called g. ">) = << , "called g. ">, "called f. ">
问题是将一对传递给函数并不能满足您的需求.但是,如果你可以将一对输入函数,该怎么办:
feed(f, feed(g, wrap(x))) = feed(f, feed(g,)) = feed(f, ) =
读feed(f, m)
为"feed m
into f
".为了养活一对
成功能f
是通过 x
进f
,拿到
了的f
,并返回
.
feed(f,) := let = f(x) in
请注意当您使用函数执行三项操作时会发生什么:
第一:如果包裹一个值,然后进料所得的一对成一个函数:
feed(f, wrap(x)) = feed(f,) = let = f(x) in = let = in = = = f(x)
这与将值传递给函数相同.
第二:如果你喂一对wrap
:
feed(wrap,) = let = wrap(x) in = let = in = =
这不会改变这对.
第三:如果你定义一个接受x
并g(x)
输入的函数f
:
h(x) := feed(f, g(x))
并喂一对:
feed(h,) = let = h(x) in = let = feed(f, g(x)) in = let = feed(f, ) in = let = let = f(x) in in = let = let = in in = let = in = = feed(f, ) = feed(f, feed(g, ))
这与将该对进g
料并将所得的对喂入其中相同f
.
你有大部分单子.现在您只需要了解程序中的数据类型.
什么类型的价值
?那么,这取决于什么类型的价值x
.如果x
是类型t
,那么您的对是"对t
和字符串" 类型的值.拨打该类型M t
.
M
是一个类型构造函数:M
单独不引用类型,但是M _
一旦用类型填充空白就引用类型.An M int
是一对int和一个字符串.An M string
是一对字符串和一个字符串.等等.
恭喜你,你创造了一个monad!
在形式上,你的monad是元组
.
monad是一个元组
,其中:
M
是一个类型构造函数.
feed
采取a(取a t
并返回a的函数M u
)和a M t
并返回a M u
.
wrap
拿a v
并返回一个M v
.
t
,, u
和v
是任何三种类型,可能相同也可能不相同.monad满足您为特定monad证明的三个属性:
馈送包裹t
成一个函数是相同的传递展开的t
入功能.
形式上: feed(f, wrap(x)) = f(x)
喂食的M t
成wrap
什么都不做的M t
.
形式上: feed(wrap, m) = m
将M t
(调用它m
)送入一个函数
通t
成g
得到一个M u
(称之为n
)g
饲料n
入f
是相同的
饲养m
成g
获得n
从g
饲养n
成f
形式:feed(h, m) = feed(f, feed(g, m))
在哪里h(x) := feed(f, g(x))
通常,feed
被称为bind
(>>=
Haskell中的AKA )并被wrap
称为return
.
我将尝试Monad
在Haskell的背景下进行解释。
在函数编程中,函数组成很重要。它允许我们的程序包含小的,易于阅读的功能。
假设我们有两个功能:g :: Int -> String
和f :: String -> Bool
。
我们可以做(f . g) x
,与一样f (g x)
,其中x
一个Int
值。
在将一个函数的结果合成/应用到另一个函数时,使类型匹配非常重要。在上述情况下,所返回的结果的类型g
必须与所接受的类型相同f
。
但是有时候值是在上下文中的,这使得排队类型变得不那么容易。(在上下文中具有值非常有用。例如,Maybe Int
类型表示Int
可能不存在IO String
的String
值,类型表示由于执行某些副作用而存在的值。)
假设我们现在有g1 :: Int -> Maybe String
和f1 :: String -> Maybe Bool
。g1
和f1
分别与g
和非常相似f
。
我们不能做(f1 . g1) x
或f1 (g1 x)
,其中x
是一个Int
值。返回的结果类型g1
不是f1
预期的。
我们可以撰写f
和g
与.
运营商,但现在我们不能组成f1
和g1
使用.
。问题是我们不能直接将上下文中的值传递给期望不在上下文中的值的函数。
如果我们引入一个运算符来组成g1
and f1
,这样我们可以写,那不是很好(f1 OPERATOR g1) x
吗?g1
返回上下文中的值。该值将脱离上下文并应用于f1
。是的,我们有这样的运营商。是<=<
。
>>=
尽管语法略有不同,但我们还有一个运算符可以为我们做完全相同的事情。
我们写:g1 x >>= f1
。g1 x
是一个Maybe Int
值。该>>=
操作有助于采取Int
值了“也许-不存在”断章取义,并将其应用到f1
。的结果f1
是Maybe Bool
,将是整个>>=
运算的结果。
最后,为什么Monad
有用?因为Monad
是定义>>=
运算符的Eq
类型类,所以==
与定义and /=
运算符的类型类非常相似。
总而言之,Monad
类型类定义了>>=
运算符,该运算符允许我们将上下文中的值(我们称这些单价)传递给在上下文中不期望值的函数。上下文将得到照顾。
如果这里要记住一件事,那就是Monad
允许函数组合涉及上下文中的值。
{-# LANGUAGE InstanceSigs #-}
newtype Id t = Id t
instance Monad Id where
return :: t -> Id t
return = Id
(=<<) :: (a -> Id b) -> Id a -> Id b
f =<< (Id x) = f x
$
函数的应用运算符
forall a b. a -> b
规范定义
($) :: (a -> b) -> a -> b
f $ x = f x
infixr 0 $
就Haskell原始函数应用f x
(infixl 10
)而言。
组成.
定义$
为
(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = \ x -> f $ g x
infixr 9 .
并满足等效条件 forall f g h.
f . id = f :: c -> d Right identity
id . g = g :: b -> c Left identity
(f . g) . h = f . (g . h) :: a -> d Associativity
.
是关联的,id
是左右身份。
在编程中,monad是函子类型构造函数,具有monad类型类的实例。定义和实现有几种等效的变体,每种变体对monad抽象的理解都略有不同。
一个仿函数是一种构造f
一种* -> *
与仿函数类型的类的实例。
{-# LANGUAGE KindSignatures #-}
class Functor (f :: * -> *) where
map :: (a -> b) -> (f a -> f b)
除遵循静态强制类型协议外,函子类型类的实例还必须遵守代数函子定律 forall f g.
map id = id :: f t -> f t Identity
map f . map g = map (f . g) :: f a -> f c Composition / short cut fusion
函子计算具有以下类型
forall f t. Functor f => f t
一个计算c r
包括在结果 r
中的上下文 c
。
一元单子函数或Kleisli箭头具有以下类型
forall m a b. Functor m => a -> m b
Kleisi箭头是具有一个参数a
并返回一元计算的函数m b
。
Monads是按照Kleisli三元规范定义的 forall m. Functor m =>
(m, return, (=<<))
实现为类型类
class Functor m => Monad m where
return :: t -> m t
(=<<) :: (a -> m b) -> m a -> m b
infixr 1 =<<
所述Kleisli身份 return
是Kleisli箭头促进的值t
成一元上下文m
。Extension或Kleisli应用程序 =<<
将Kleisli箭头应用于a -> m b
计算结果m a
。
Kleisli组成 <=<
根据扩展定义为
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
f <=< g = \ x -> f =<< g x
infixr 1 <=<
<=<
组成两个Kleisli箭头,将左箭头应用于右箭头应用的结果。
monad类型类的实例必须遵守monad法则,就Kleisli组成而言,其用法最为优美:forall f g h.
f <=< return = f :: c -> m d Right identity
return <=< g = g :: b -> m c Left identity
(f <=< g) <=< h = f <=< (g <=< h) :: a -> m d Associativity
<=<
是关联的,return
是左右身份。
身份类型
type Id t = t
是类型上的标识函数
Id :: * -> *
解释为函子,
return :: t -> Id t
= id :: t -> t
(=<<) :: (a -> Id b) -> Id a -> Id b
= ($) :: (a -> b) -> a -> b
(<=<) :: (b -> Id c) -> (a -> Id b) -> (a -> Id c)
= (.) :: (b -> c) -> (a -> b) -> (a -> c)
在规范的Haskell中,标识monad被定义
newtype Id t = Id t
instance Functor Id where
map :: (a -> b) -> Id a -> Id b
map f (Id x) = Id (f x)
instance Monad Id where
return :: t -> Id t
return = Id
(=<<) :: (a -> Id b) -> Id a -> Id b
f =<< (Id x) = f x
选项类型
data Maybe t = Nothing | Just t
Maybe t
对不一定会产生结果的t
计算进行编码,即可能“失败”的计算。选项monad已定义
instance Functor Maybe where
map :: (a -> b) -> (Maybe a -> Maybe b)
map f (Just x) = Just (f x)
map _ Nothing = Nothing
instance Monad Maybe where
return :: t -> Maybe t
return = Just
(=<<) :: (a -> Maybe b) -> Maybe a -> Maybe b
f =<< (Just x) = f x
_ =<< Nothing = Nothing
a -> Maybe b
仅当Maybe a
产生结果时才应用于结果。
newtype Nat = Nat Int
可以将自然数编码为大于或等于零的那些整数。
toNat :: Int -> Maybe Nat
toNat i | i >= 0 = Just (Nat i)
| otherwise = Nothing
减法不会关闭自然数。
(-?) :: Nat -> Nat -> Maybe Nat
(Nat n) -? (Nat m) = toNat (n - m)
infixl 6 -?
选项monad涵盖了异常处理的基本形式。
(-? 20) <=< toNat :: Int -> Maybe Nat
列表单子,在列表类型之上
data [] t = [] | t : [t]
infixr 5 :
并且其附加的monoid操作“附加”
(++) :: [t] -> [t] -> [t]
(x : xs) ++ ys = x : xs ++ ys
[] ++ ys = ys
infixr 5 ++
对非线性计算进行编码[t]
产生自然0, 1, ...
的结果t
。
instance Functor [] where
map :: (a -> b) -> ([a] -> [b])
map f (x : xs) = f x : map f xs
map _ [] = []
instance Monad [] where
return :: t -> [t]
return = (: [])
(=<<) :: (a -> [b]) -> [a] -> [b]
f =<< (x : xs) = f x ++ (f =<< xs)
_ =<< [] = []
扩展将将Kleisli箭头应用于的元素所得到的所有列表=<<
连接到单个结果列表中。++
[b]
f x
a -> [b]
[a]
[b]
让一个正整数的正确除数n
是
divisors :: Integral t => t -> [t]
divisors n = filter (`divides` n) [2 .. n - 1]
divides :: Integral t => t -> t -> Bool
(`divides` n) = (== 0) . (n `rem`)
然后
forall n. let { f = f <=< divisors } in f n = []
在定义monad类型类而不是扩展时=<<
,Haskell标准使用其翻转字符bind运算符>>=
。
class Applicative m => Monad m where
(>>=) :: forall a b. m a -> (a -> m b) -> m b
(>>) :: forall a b. m a -> m b -> m b
m >> k = m >>= \ _ -> k
{-# INLINE (>>) #-}
return :: a -> m a
return = pure
为简单起见,此说明使用类型类层次结构
class Functor f
class Functor m => Monad m
在Haskell中,当前的标准层次结构为
class Functor f
class Functor p => Applicative p
class Applicative m => Monad m
因为不仅每个monad都是函子,而且每个应用程序都是函子,每个monad也都是应用程序。
使用列表monad,命令伪代码
for a in (1, ..., 10)
for b in (1, ..., 10)
p <- a * b
if even(p)
yield p
大致翻译为do块,
do a <- [1 .. 10]
b <- [1 .. 10]
let p = a * b
guard (even p)
return p
等效的monad理解,
[ p | a <- [1 .. 10], b <- [1 .. 10], let p = a * b, even p ]
和表达
[1 .. 10] >>= (\ a ->
[1 .. 10] >>= (\ b ->
let p = a * b in
guard (even p) >> -- [ () | even p ] >>
return p
)
)
注解和monad理解是嵌套绑定表达式的语法糖。bind操作符用于单子结果的本地名称绑定。
let x = v in e = (\ x -> e) $ v = v & (\ x -> e)
do { r <- m; c } = (\ r -> c) =<< m = m >>= (\ r -> c)
哪里
(&) :: a -> (a -> b) -> b
(&) = flip ($)
infixl 0 &
保护功能已定义
guard :: Additive m => Bool -> m ()
guard True = return ()
guard False = fail
其中单元类型或“空的元组”
data () = ()
可以使用类型类抽象化支持选择和失败的加法蒙纳德
class Monad m => Additive m where
fail :: m t
(<|>) :: m t -> m t -> m t
infixl 3 <|>
instance Additive Maybe where
fail = Nothing
Nothing <|> m = m
m <|> _ = m
instance Additive [] where
fail = []
(<|>) = (++)
其中fail
并<|>
形成一幺forall k l m.
k <|> fail = k
fail <|> l = l
(k <|> l) <|> m = k <|> (l <|> m)
并且fail
是添加单核的吸收//灭零元素
_ =<< fail = fail
如果在
guard (even p) >> return p
even p
为真,则警卫队产生[()]
,并根据的定义>>
,产生局部常数
\ _ -> return p
适用于结果()
。如果为假,则后卫将生成列表monad的fail
([]
),将其应用于Kleisli箭头不会产生任何结果>>
,因此将p
其跳过。
臭名昭著的是,使用monad对状态计算进行编码。
甲状态处理器是一个函数
forall st t. st -> (t, st)
转换状态st
并产生结果t
。该状态 st
可以是任何东西。没有任何内容,标志,计数,数组,句柄,机器,世界。
状态处理器的类型通常称为
type State st t = st -> (t, st)
状态处理程序monad是仁慈的* -> *
函子State st
。状态处理器monad的Kleisli箭头是函数
forall st a b. a -> (State st) b
在规范的Haskell中,定义了状态处理器monad的惰性版本
newtype State st t = State { stateProc :: st -> (t, st) }
instance Functor (State st) where
map :: (a -> b) -> ((State st) a -> (State st) b)
map f (State p) = State $ \ s0 -> let (x, s1) = p s0
in (f x, s1)
instance Monad (State st) where
return :: t -> (State st) t
return x = State $ \ s -> (x, s)
(=<<) :: (a -> (State st) b) -> (State st) a -> (State st) b
f =<< (State p) = State $ \ s0 -> let (x, s1) = p s0
in stateProc (f x) s1
通过提供初始状态来运行状态处理器:
run :: State st t -> st -> (t, st)
run = stateProc
eval :: State st t -> st -> t
eval = fst . run
exec :: State st t -> st -> st
exec = snd . run
状态访问由原语get
和put
,有状态单子抽象方法提供:
{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}
class Monad m => Stateful m st | m -> st where
get :: m st
put :: st -> m ()
m -> st
声明状态类型对monad 的功能依赖 ; 一个,例如,将确定的状态类型是唯一。st
m
State t
t
instance Stateful (State st) st where
get :: State st st
get = State $ \ s -> (s, s)
put :: st -> State st ()
put s = State $ \ _ -> ((), s)
与void
在C中类似使用的单位类型。
modify :: Stateful m st => (st -> st) -> m ()
modify f = do
s <- get
put (f s)
gets :: Stateful m st => (st -> t) -> m t
gets f = do
s <- get
return (f s)
gets
通常与记录字段访问器一起使用。
状态monad等效于变量线程
let s0 = 34
s1 = (+ 1) s0
n = (* 12) s1
s2 = (+ 7) s1
in (show n, s2)
其中s0 :: Int
,是同样参照透明的,但无限地更加优雅和实用
(flip run) 34
(do
modify (+ 1)
n <- gets (* 12)
modify (+ 7)
return (show n)
)
modify (+ 1)
是类型的计算State Int ()
,除了其作用等同于return ()
。
(flip run) 34
(modify (+ 1) >>
gets (* 12) >>= (\ n ->
modify (+ 7) >>
return (show n)
)
)
结合性的莫纳德定律可以写成 >>=
forall m f g.
(m >>= f) >>= g = m >>= (\ x -> f x >>= g)
要么
do { do { do {
r1 <- do { x <- m; r0 <- m;
r0 <- m; = do { = r1 <- f r0;
f r0 r1 <- f x; g r1
}; g r1 }
g r1 }
} }
像面向表达式的编程(例如Rust)一样,块的最后一条语句表示其产量。绑定运算符有时称为“可编程分号”。
一元模拟来自结构化命令式编程的迭代控制结构原语
for :: Monad m => (a -> m b) -> [a] -> m ()
for f = foldr ((>>) . f) (return ())
while :: Monad m => m Bool -> m t -> m ()
while c m = do
b <- c
if b then m >> while c m
else return ()
forever :: Monad m => m t
forever m = m >> forever m
data World
I / O世界状态处理器monad是纯Haskell与现实世界,功能性说明性和命令性操作语义的协调。与实际严格执行的近似:
type IO t = World -> (t, World)
不纯净的原语促进了交互
getChar :: IO Char
putChar :: Char -> IO ()
readFile :: FilePath -> IO String
writeFile :: FilePath -> String -> IO ()
hSetBuffering :: Handle -> BufferMode -> IO ()
hTell :: Handle -> IO Integer
. . . . . .
使用IO
原语的代码的杂物由类型系统永久地协议化。因为纯净真棒,所以发生在里面IO
,留在里面IO
。
unsafePerformIO :: IO t -> t
或者至少应该如此。
Haskell程序的类型签名
main :: IO ()
main = putStrLn "Hello, World!"
扩展到
World -> ((), World)
改变世界的功能。
类别对象是Haskell类型,而态态是Haskell类型之间的函数,类别为“快速和宽松” Hask
。
函子T
是从类别C
到类别的映射D
; 对于一个对象中C
的每个对象D
Tobj : Obj(C) -> Obj(D)
f :: * -> *
并且对于一个态中C
的每个态D
Tmor : HomC(X, Y) -> HomD(Tobj(X), Tobj(Y))
map :: (a -> b) -> (f a -> f b)
其中X
,Y
在对象C
。HomC(X, Y)
是同态类的所有态射的X -> Y
在C
。仿函数必须保留态射特性和组合物,所述的“结构” C
中D
。
Tmor Tobj
T(id) = id : T(X) -> T(X) Identity
T(f) . T(g) = T(f . g) : T(X) -> T(Z) Composition
该Kleisli类一类C
是由Kleisli三重给定
终结者的
T : C -> C
(f
),身份同构eta
(return
)和扩展运算符*
(=<<
)。
每个Kleisli态 Hask
f : X -> T(Y)
f :: a -> m b
由扩展运营商
(_)* : Hom(X, T(Y)) -> Hom(T(X), T(Y))
(=<<) :: (a -> m b) -> (m a -> m b)
在Hask
的Kleisli类别中被赋予了态射
f* : T(X) -> T(Y)
(f =<<) :: m a -> m b
克莱斯里(Kleisli)类别的构成以.T
扩展名给出
f .T g = f* . g : X -> T(Z)
f <=< g = (f =<<) . g :: a -> m c
并满足类别公理
eta .T g = g : Y -> T(Z) Left identity
return <=< g = g :: b -> m c
f .T eta = f : Z -> T(U) Right identity
f <=< return = f :: c -> m d
(f .T g) .T h = f .T (g .T h) : X -> T(U) Associativity
(f <=< g) <=< h = f <=< (g <=< h) :: a -> m d
应用等价转换
eta .T g = g
eta* . g = g By definition of .T
eta* . g = id . g forall f. id . f = f
eta* = id forall f g h. f . h = g . h ==> f = g
(f .T g) .T h = f .T (g .T h)
(f* . g)* . h = f* . (g* . h) By definition of .T
(f* . g)* . h = f* . g* . h . is associative
(f* . g)* = f* . g* forall f g h. f . h = g . h ==> f = g
在扩展方面被规范地给出
eta* = id : T(X) -> T(X) Left identity
(return =<<) = id :: m t -> m t
f* . eta = f : Z -> T(U) Right identity
(f =<<) . return = f :: c -> m d
(f* . g)* = f* . g* : T(X) -> T(Z) Associativity
(((f =<<) . g) =<<) = (f =<<) . (g =<<) :: m a -> m c
也可以mu
在编程中称为Monad,而不是根据Kleislian扩展定义而是自然转换join
。monad被定义mu
为C
endofunctor 的category 的三元组
T : C -> C
f :: * -> *
和两个自然转变
eta : Id -> T
return :: t -> f t
mu : T . T -> T
join :: f (f t) -> f t
满足等价