最近对Haskell进行了简要介绍后,关于monad本质上是什么的简要而简洁的实际解释是什么?
我发现我所遇到的大多数解释都是相当难以理解的,并且缺乏实际细节。
最近对Haskell进行了简要介绍后,关于monad本质上是什么的简要而简洁的实际解释是什么?
我发现我所遇到的大多数解释都是相当难以理解的,并且缺乏实际细节。
Answers:
首先:如果您不是数学家,那么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时,操作被链接在一起,以便它们按顺序执行,除非抛出错误,在这种情况下,链的其余部分将被放弃。
列表理解语法和do表示法都是使用>>=
操作符进行链接操作的语法糖。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
monad可以自己“拆分”该块并推迟执行后半部分。(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类型类的实例,该类>>=
与其他一些运算符一起定义。用外行术语来说,单子仅是为其>>=
定义操作的类型。
它本身>>=
只是一种繁琐的函数链接方式,但是由于存在隐藏“管道”的do-notation,所以monadic操作被证明是非常好的和有用的抽象,在语言中的很多地方都有用,并且有用用于创建自己的语言的迷你语言。
为什么单子很难?
对于许多Haskell学习者而言,单子电池就像壁垒一样是一个障碍。并不是说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
解释“什么是单子”有点像说“什么是数字?” 我们一直在使用数字。但是想象一下您遇到了一个对数字一无所知的人。如何赫克你能解释的数字是什么?您甚至将如何描述它可能有用的原因?
什么是单子?简短的答案:这是将操作链接在一起的一种特定方式。
本质上,您正在编写执行步骤,并将它们与“绑定函数”链接在一起。(在Haskell中,它的名称为>>=
。)您可以自己编写对bind运算符的调用,也可以使用语法糖使编译器为您插入这些函数调用。但是无论哪种方式,每个步骤都由对该绑定函数的调用分隔。
因此,bind函数就像一个分号;它分离了流程中的步骤。绑定函数的工作是获取上一步的输出,并将其输入到下一步。
听起来不太难,对吧?但是不止一种monad。为什么?怎么样?
好了,bind函数可以只从一个步骤中获取结果,然后将其提供给下一步。但是,如果那是“全部”,那单子弹就可以了……那实际上不是很有用。理解这一点很重要:每个有用的 monad 除了成为monad 之外,还会执行其他操作。每个有用的单子都具有“特殊能力”,这使其具有独特性。
(一个没有任何特殊要求的monad称为“ identity monad”。这与identity函数一样,听起来似乎毫无意义,但事实并非如此……但这是另一个故事™。)
基本上,每个monad都有其自己的bind函数实现。而且,您可以编写一个绑定函数,使其在执行步骤之间做一些令人讨厌的事情。例如:
如果每个步骤都返回成功/失败指示符,则只有在前一个步骤成功的情况下,您才能让绑定执行下一个步骤。这样,失败的步骤会“自动”中止整个序列,而无需您进行任何条件测试。(失败单子。)
扩展这个想法,您可以实现“异常”。(Error Monad或Exception Monad。)由于您是自己定义它们,而不是作为一种语言功能,因此可以定义它们的工作方式。(例如,也许您想忽略前两个异常,而仅在抛出第三个异常时才中止。)
您可以使每个步骤返回多个结果,并使bind函数遍历它们,将每个结果反馈给您。这样,在处理多个结果时,您不必一直在各处编写循环。绑定功能“自动”为您完成所有操作。(列表Monad。)
除了将“结果”从一个步骤传递到另一步骤外,您还可以让bind函数传递额外的数据。现在,这些数据不会显示在您的源代码中,但是您仍然可以从任何地方访问它,而不必手动将其传递给每个函数。(读者Monad。)
您可以这样做,以便替换“额外数据”。这使您可以模拟破坏性更新,而无需实际进行破坏性更新。(国家Monad及其堂兄作家Monad。)
因为您仅模拟破坏性更新,所以您可以琐碎地执行真正的破坏性更新无法完成的任务。例如,您可以撤消上一次更新,或恢复为旧版本。
您可以将monad放在可以暂停计算的位置,这样您可以暂停程序,进入并修改内部状态数据,然后继续执行。
您可以将“ continuations”实现为monad。这可以让您放心!
使用monad可以实现所有这些以及更多功能。当然,如果没有 monad ,所有这些都是完全可能的。这只是大幅容易使用的单子。
实际上,与对Monad的普遍理解相反,它们与国家无关。Monad只是包装事物的一种方法,并且提供了对包装的事物进行操作而无需对其进行包装的方法。
例如,您可以在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。
至于了解单子,不要太担心。了解他们的有趣之处,如果您不立即了解,也不必担心。然后,仅使用Haskell这样的语言进行潜水就可以了。Monad是其中的一种,通过练习,理解会渗入大脑,有一天,您突然意识到自己已经理解它们。
sigfpe说:
但是所有这些都将monads引入作为需要解释的深奥事物。但是我想说的是,它们根本不是深奥的。实际上,面对函数式编程中的各种问题,您不可避免地会遇到某些解决方案,所有这些都是monad的示例。实际上,我希望您现在就发明它们。然后,迈出一小步即可注意到所有这些解决方案实际上都是变相的相同解决方案。阅读完此书后,您可能会更好地了解monad上的其他文档,因为您将认识到所有已被发明的东西。
Monad尝试解决的许多问题都与副作用有关。因此,我们将从它们开始。(请注意,单子命令不仅可以处理副作用,而且可以将许多类型的容器对象视为单子命令。对单子命令的一些介绍发现,很难兼顾这两种不同的单子用法,而仅专注于一个或多个。另一个。)
在命令式编程语言(例如C ++)中,函数的行为与数学函数完全不同。例如,假设我们有一个C ++函数,该函数采用单个浮点参数并返回浮点结果。从表面上看,它似乎有点像一个将实数映射为实数的数学函数,但是C ++函数可以做的不仅仅是返回一个取决于其参数的数字。它可以读取和写入全局变量的值,也可以将输出写入屏幕并从用户那里接收输入。但是,在纯函数式语言中,函数只能读取其参数中提供给它的内容,而对世界产生影响的唯一方法是通过其返回的值。
monad是具有两种操作的数据类型:>>=
(aka bind
)和return
(aka unit
)。return
接受任意值并使用它创建monad的实例。>>=
接受monad的一个实例并在其上映射一个函数。(您已经知道monad是一种奇怪的数据类型,因为在大多数编程语言中,您无法编写一个采用任意值并从中创建类型的函数。monad使用一种参数多态性。)
用Haskell表示法编写monad接口
class Monad m where
return :: a -> m a
(>>=) :: forall a b . m a -> (a -> m b) -> m b
这些操作应该遵守某些“法则”,但这并不是很重要的:“法则”只是编纂了合理的操作实现方式(基本上,>>=
并且return
应该就如何将值转换为monad实例达成共识)。这>>=
是关联的)。
Monad不仅涉及状态和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
构造函数。这两个单子都在它们各自的数据类型上封装了通用且有用的计算模式(请注意,两者均与副作用或I / O没有任何关系)。
您确实必须尝试编写一些平凡的Haskell代码,以了解monad的含义以及它们为何有用。
您首先应该了解什么是函子。在此之前,请先了解高阶函数。
甲高阶函数是一个简单的函数,它的功能作为一个参数。
甲算符是任何类型的构造T
为其中存在一个高阶函数,调用它map
,该变换类型的函数a -> b
(给定任何两种类型的a
和b
)成一个函数T a -> T b
。此map
函数还必须遵守身份和组成定律,以使以下表达式对all p
和q
(Haskell表示法)返回true :
map id = id
map (p . q) = map p . map q
例如,如果一个类型构造函数被称为List
functor,则它具有(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实例的方式。
一个单子必须满足某些法律,即join
必须具有关联性。这意味着,如果你有一个价值x
型的[[[a]]]
则join (join x)
应该等于join (map join x)
。而且pure
必须是一个身份join
,使得join (pure x) == x
。
[免责声明:我仍在尝试完全使用monad。以下是到目前为止我所了解的。如果错了,希望有知识的人会在地毯上给我打电话。]
阿尔纳尔写道:
Monad只是包装事物的一种方法,并且提供了对包装的事物进行操作而无需对其进行包装的方法。
就是这样。这个想法是这样的:
您需要某种价值,并在其中附加一些其他信息。就像值是某种类型(例如整数或字符串)一样,附加信息也是某种类型。
例如,多余的信息可能是Maybe
或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只是普通数字,并对它们执行常规数学运算。如果要获得a Nothing
,您的方程式仍将产生正确的结果- 无需在Nothing
任何地方乱扔检查。
但是这个例子正是发生了什么Maybe
。如果额外的信息是IO
,则将IO
调用为s 定义的特殊运算符,它可以在执行加法之前做一些完全不同的事情。(好吧,将两个IO Int
s加在一起可能是无意义的-我不确定。)(此外,如果您关注Maybe
示例,您会注意到“用额外的东西包装价值”并不总是正确的。但这很难准确,正确和精确而又不言而喻。)
基本上,“ monad”大致表示“模式”。但是,现在不再是一本充满非正式解释和专门命名的模式的书,而是现在有了一种语言构造 -语法和全部-可让您在程序中将新的样式声明为事物。(这里的不精确之处在于所有模式都必须遵循特定的形式,因此monad并不像模式那样通用。但是我认为这是大多数人都知道和理解的最接近的术语。)
这就是为什么人们会觉得单子如此令人困惑:因为它们是一个通用的概念。问什么使某物成为单子与问什么使某物成为一种模式相似。
但是,请考虑一下在语言中对模式的想法提供语法支持的含义:不必阅读《四人帮》一书并记住特定模式的构造,只需编写以不可知论的方式实现该模式的代码,一般的方法一次就完成了!然后,您可以重复使用这种模式,例如Visitor或Strategy或Façade,等等,只需用它装饰代码中的操作即可,而不必一遍又一遍地重新实现!
这就是为什么了解 monad的人会发现它们如此有用的原因:知识分子势利的人以理解为荣并不是象牙塔的概念(当然,当然也是如此),但是实际上使代码更简单。
M (M a) -> M a
。您可以将其转换为一种类型的事实M a -> (a -> M b) -> M b
是使它们有用的原因。
经过多方努力,我认为我终于了解了单子。在重新阅读了我对压倒性多数投票的冗长评论之后,我将提供这种解释。
要了解单子,需要回答三个问题:
正如我在原始评论中指出的那样,太多的单子论解释陷入了第3个问题,而没有真正涵盖第2个问题或第一个问题。
为什么需要单子?
像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%!");
实际上,这将是一种更方便的编写方式。但是我们该怎么做呢?
什么是单子?
一旦consolestate
定义了一个类型(例如)以及专门设计用于对该类型进行操作的一堆函数,就可以通过定义一个:
自动(如(绑定))运算符将这些东西的整个包变成“ monad” 将左侧的返回值输入右侧的函数参数,并将lift
正常函数转换为与该特定种类的绑定运算符一起使用的函数的运算符。
monad如何实现?
看到其他答案,似乎很容易进入其中。
几年前给出了这个问题的答案后,我相信我可以通过...来改善和简化该回答。
monad是一种功能组合技术,它使用组合功能外部化某些输入方案的处理bind
,以在组合期间对输入进行预处理。
在正常组合中,函数compose (>>)
用来将组合的函数按顺序应用于其前任的结果。重要的是,需要组合函数来处理其输入的所有情况。
(x -> y) >> (y -> z)
可以通过重组输入来改进此设计,以便更轻松地查询相关状态。因此,例如,如果包括有效性概念,则y
值可能会变得不那么简单。Mb
(is_OK, b)
y
例如,当输入仅可能是数字时,可以返回bool
一个有效的数字和元组中的数字(例如),而不是返回可以包含或不包含一个数字的字符串,而可以将类型重组为bool * float
。现在,组合函数将不再需要解析输入字符串来确定是否存在数字,而只需检查bool
元组的一部分。
(Ma -> Mb) >> (Mb -> Mc)
同样,这里的组合自然发生了compose
,因此每个函数必须分别处理其输入的所有情况,尽管这样做现在要容易得多。
但是,如果我们可以将处理情景的常规时间的审讯工作外化,该怎么办。例如,如果输入不正确(如when is_OK
is)时程序不执行任何操作,该怎么办false
?如果这样做了,则组合函数将不需要自己处理该场景,从而大大简化了它们的代码并实现了另一级重用。
为了实现这种外在化,我们可以使用函数bind (>>=)
来composition
代替compose
。因此,与其简单地将值从一个函数的输出传递到另一个函数的输入,Bind
不如检查其M
一部分Ma
并决定是否以及如何将组合函数应用于a
。当然,该功能bind
将针对我们的特定情况进行定义,M
以便能够检查其结构并执行我们想要的任何类型的应用程序。尽管如此,因为a
可以bind
仅通过的,所以可以是任何东西a
在确定应用程序必要时才检查的给组合函数。此外,组合函数本身不再需要处理M
输入结构的一部分,以简化它们。因此...
(a -> Mb) >>= (b -> Mc)
或更简洁 Mb >>= (b -> Mc)
简而言之,一旦输入被设计为足以暴露它们,那么monad就会外部化,从而围绕某些输入方案提供标准行为。此设计是一个shell and content
模型,其中外壳包含与组合功能的应用程序相关的数据,并由该bind
功能询问并且仅对功能可用。
因此,单子是三件事:
M
用于存放monad相关信息的外壳, bind
实现该功能的函数,用于在将组合函数应用于它在外壳中找到的内容值时使用此外壳信息,以及 a -> Mb
,产生包含单子管理数据的结果。一般而言,函数的输入比其输出的约束要严格得多,输出可能包括错误条件。因此,Mb
结果结构通常非常有用。例如,除数为时,除法运算符不返回数字0
。
此外,monad
s可能包括包装函数,这些包装函数通过在应用后将其结果包装起来,从而将值包裹a
到monadic类型中Ma
,并将通用函数包裹a -> b
到monadic函数中a -> Mb
。当然,类似bind
这样的包装函数特定于M
。一个例子:
let return a = [a]
let lift f a = return (f a)
该bind
函数的设计假定不变的数据结构和纯函数,而其他事情则变得复杂而无法保证。因此,有单子法则:
鉴于...
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)
Associativity
表示bind
无论何时bind
应用,都保留评估顺序。也就是说,在定义Associativity
以上,力括号内的早期评估binding
的f
,并g
只会导致一个功能,预计Ma
为了完成bind
。因此,Ma
必须先确定的评估,然后才能将其值应用到,f
并将结果应用到g
。
单子实际上是“类型运算符”的一种形式。它将做三件事。首先,它将“包装”(或以其他方式)将一种类型的值转换为另一种类型(通常称为“单子类型”)。其次,它将使基础类型上可用的所有操作(或函数)在单子类型上可用。最后,它将为将自身与另一个monad结合以产生复合monad提供支持。
在Visual Basic / C#中,“也许单子”本质上等效于“可空类型”。它采用非空类型“ T”,并将其转换为“ Nullable <T>”,然后定义所有二进制运算符对Nullable <T>的含义。
副作用被相似地表示。将创建一个结构,其中包含副作用的描述以及函数的返回值。然后,在函数之间传递值时,“提升”操作会在副作用周围进行复制。
它们被称为“ monads”,而不是“类型运算符”更易于掌握的名称,原因如下:
sigfpe(Dan Piponi)的《您可能已经发明了Monads》是Monads的一个很好动机!(也许您已经拥有了)。还有很多其他的monad教程,其中许多使用各种类比方法以错误的方式试图用“简单的术语”来解释monad:这是monad的谬论;避免他们。
正如MacIver博士在告诉我们您的语言为何糟透了:
因此,我讨厌Haskell:
让我们从显而易见的地方开始。Monad教程。不,不是单子。特别是教程。他们是无尽的,夸张的,亲爱的上帝,他们乏味。此外,我从未见过任何令人信服的证据证明它们确实有所帮助。阅读类定义,编写一些代码,克服可怕的名字。
您说您了解Maybe monad吗?好,您在路上。刚开始使用其他monad,迟早您将了解一般的monad。
[如果您以数学为导向,则可能要忽略数十个教程并学习其定义,或者按照类别理论进行讲授:)定义的主要部分是Monad M涉及为每个对象定义的“类型构造函数”现有类型“ T”和新类型“ MT”,以及在“常规”类型和“ M”类型之间往返的一些方法。
同样,令人惊讶的是,对monad的最佳介绍之一实际上是介绍monad的早期学术论文之一,即Philip Wadler的函数编程的Monad。实际上,它有一些实用的,不平凡的激励性示例,与现有的许多人工教程不同。
Monad将控制流的抽象数据类型是数据。
换句话说,许多开发人员对集合,列表,字典(或哈希或地图)和树的想法感到满意。在这些数据类型中,有许多特殊情况(例如InsertionOrderPreservingIdentityHashMap)。
但是,面对程序“流程”时,许多开发人员没有比if,switch / case,do,while,goto(grr)和(也许)闭包更多的结构。
因此,monad仅仅是控制流的构造。代替monad的更好的说法是“控件类型”。
这样,monad具有用于控制逻辑,语句或函数的插槽-数据结构中的等效项是表示某些数据结构允许您添加和删除数据。
例如,“ if”单子:
if( clause ) then block
最简单的有两个插槽-一个子句和一个块。该if
单子通常是建立评估条款的结果,如果不是假的,评估该块。许多开发人员在学习“ if”时并没有被介绍给monad,因此不必理解monad来编写有效的逻辑。
Monad可能变得更加复杂,就像数据结构可能变得更加复杂一样,但是monad的许多广泛类别可能具有相似的语义,但是实现和语法不同。
当然,以与可以遍历或遍历数据结构相同的方式,可以评估单子。
编译器可能支持也可能不支持用户定义的monad。Haskell当然可以。Ioke具有一些类似的功能,尽管该语言未使用术语monad。
我最喜欢的Monad教程:
http://www.haskell.org/haskellwiki/All_About_Monads
(在Google搜索“ monad教程”的170,000次点击中!)
@Stu:monad的要点是允许您向通常为纯代码的地方添加(通常)顺序语义;您甚至可以编写monad(使用Monad Transformers)并获得更有趣和更复杂的组合语义,例如使用错误处理,共享状态和日志记录进行解析。所有这一切都可以用纯代码实现,monads仅允许您将其抽象出来并在模块化库中重用(总是擅长编程),并提供方便的语法使其显得势在必行。
Haskell已经有运算符重载[1]:它使用类型类的方式与人们可能在Java或C#中使用接口的方式很相似,但是Haskell恰好也允许非字母数字标记(如+ &&和>)用作中缀标识符。如果您的意思是“重载分号” [2],那么这只是运算符在您查看方式中的重载。听起来像是黑魔法,并要求麻烦“使分号超载”(画面敏锐的Perl黑客对此想法颇有兴趣),但要点是,没有monads就不会有分号,因为纯功能代码不需要或不允许显式排序。
这听起来比需要的要复杂得多。sigfpe的文章非常酷,但是使用Haskell进行了解释,这无法打破理解Haskell吞噬Monads和理解Monads吞噬Haskell的鸡蛋问题。
[1]这是与monads不同的问题,但是monads使用Haskell的运算符重载功能。
[2]这也是一个过分的简化,因为用于链接单子动作的运算符是>> =(发音为“ bind”),但是有语法糖(“ do”)可以让您使用花括号和分号和/或缩进和换行符。
最近,我一直以不同的方式想到Monads。我一直认为它们是抽象出执行顺序数学方式,这使得新型的多态性成为可能。
如果您使用命令式语言,并且按顺序编写了一些表达式,则代码始终按该顺序运行。
在简单的情况下,当您使用monad时,感觉就一样-您定义了顺序出现的表达式列表。除此之外,根据您使用的monad,您的代码可能会按顺序运行(例如在IO monad中),一次并行运行在多个项目上(例如在List monad中),它可能会中途停止运行(例如在Maybe monad中) ,它可能会中途暂停,以稍后再恢复(例如在Resumetion单子中),它可能倒退并从头开始(例如在Transaction单子中),或者可能后退以尝试其他选项(例如在Logic单子中) 。
而且由于monad是多态的,因此可以根据需要在不同的monad中运行相同的代码。
另外,在某些情况下,可以将monad组合在一起(使用monad变压器)以同时获得多个功能。
我对monad还是陌生的,但我想我应该分享一个链接,我觉得阅读起来真的很棒(有图片!!):http : //www.matusiak.eu/numerodix/blog/2012/3/11/外行monads / (无从属关系)
基本上,我从本文中得到的温暖而模糊的概念是,monads基本上是适配器,它允许不同的功能以可组合的方式工作,即能够将多个功能串起来并进行混合和匹配,而不必担心返回不一致类型等。因此,当我们尝试制作这些适配器时,BIND函数负责使苹果与苹果保持一致,橙子与橙保持一致。LIFT功能负责采用“较低级别”功能并将其“升级”以与BIND功能一起使用,并且也可以组合。
我希望我做对了,更重要的是,希望本文对monads有一个正确的看法。如果没有别的,这篇文章有助于激发我的兴趣,以了解更多有关单子的知识。
除了上述出色的答案外,让我为您提供以下文章的链接(由Patrick Thomson撰写),该文章通过将概念与JavaScript库jQuery(及其使用“方法链”操作DOM的方式)相关联来说明单子。: jQuery是Monad
在jQuery的文档本身并不是指术语“单子”,但谈到了“构建者模式”,这可能是比较熟悉的。这并不会改变您那里可能有一个适当的monad的事实,甚至没有意识到。
在实践中,monad是函数组合运算符的自定义实现,它负责处理副作用以及不兼容的输入和返回值(用于链接)。
如果我正确理解,则IEnumerable是从monad派生的。我想知道对于C#世界中的那些人来说这是否可能是一个有趣的方法?
值得一提的是,这里有一些教程的链接对我有所帮助(而且,不,我仍然不知道什么是monads)。
当我了解那里时,对我最有帮助的两件事是:
格雷厄姆·赫顿(Graham Hutton)在《 Haskell中的编程》一书中的第8章“功能解析器”。实际上,这根本没有提到monad,但是,如果您可以通读本章并真正理解其中的所有内容,尤其是如何评估绑定操作序列,那么您将了解monad的内部。期望这需要几次尝试。
本教程“关于Monads”。这给出了使用它们的几个很好的例子,我不得不说我为我工作的附录中的类比。
Monoid似乎可以确保在Monoid上定义的所有操作以及受支持的类型始终在Monoid内返回受支持的类型。例如,任何数字+任何数字=一个数字,没有错误。
而除法接受两个小数,并返回一个小数,它在haskell somewhy中将零除定义为Infinity(恰好是小数)。
无论如何,似乎Monads只是确保操作链以可预测的方式运行的一种方法,而声称为Num-> Num的函数由Num-> Num的另一个函数x组成,例如,发射导弹。
另一方面,如果我们具有可以发射导弹的功能,则可以将其与也可以发射导弹的其他功能组合在一起,因为我们的意图很明确-我们想发射导弹-但它不会尝试出于某些奇怪的原因打印“ Hello World”。
在Haskell中,main类型是IO()或IO [()],该区域很奇怪,我将不讨论它,但是我认为这是发生的情况:
如果我有main,则希望它执行一系列操作,运行该程序的原因是产生效果-通常是通过IO。因此,我可以将IO操作主要连接在一起,以便进行IO,而无需做其他事情。
如果我尝试执行不“返回IO”的操作,程序将抱怨链条不畅通,或者基本上是“这与我们要执行的操作有什么关系-IO操作”,它似乎很强制程序员可以保持思路,而不必逃避和思考发射导弹,而可以创建排序算法-这种算法不会流动。
基本上,Monads似乎是编译器的一个提示,“嘿,您知道此函数在此处返回一个数字,它实际上并不总是起作用,它有时可以产生一个Number,有时什么也没有,只要将其保留在心神”。知道这一点后,如果您尝试声明一个monadic动作,则该monadic动作可能会充当编译时异常,说明“嘿,这实际上不是数字,这可以是数字,但是您不能假设这样做,可以做点什么以确保流量可以接受。” 这可以在一定程度上防止程序行为无法预测。
似乎单子论与纯度无关,也不与控制无关,而是与保持类别的标识有关,在该类别上所有行为都是可以预测和定义的,或者不能编译。当您期望做某事时,您什么也不会做;如果您什么都不做(可见),则您将无法做某事。
我对Monads想到的最大原因是-查看过程/ OOP代码,您会发现您不知道程序从哪里开始,也没有结束,您看到的只是很多跳跃和很多数学运算,魔术和导弹。您将无法维护它,并且,如果可以的话,您将花费大量时间在整个程序上全神贯注,然后才可以理解它的任何部分,因为在这种情况下,模块化是基于相互依赖的“节”代码,其中将代码优化为尽可能相关,以保证效率/相互关系。Monad非常具体,并通过定义进行了很好的定义,并确保程序流能够分析和隔离难以分析的部分,因为它们本身就是Monad。单子似乎是“ 甚至毁灭宇宙甚至扭曲时间-我们都不知道,也没有任何保证。monad保证它就是它。这非常强大。甚至毁灭宇宙甚至扭曲时间-我们都不知道,也没有任何保证。monad保证它就是它。这非常强大。
从某种意义上说,“现实世界”中的所有事物似乎都是单子形式的,因为它受到防止混淆的明确可观察定律的约束。这并不意味着我们必须模仿该对象的所有操作来创建类,而是可以简单地说“一个正方形就是一个正方形”,只不过是一个正方形,甚至不是矩形也不是圆形,而“正方形具有面积”它是现有尺寸之一的长度乘以自身的长度,无论您拥有什么正方形,如果它是2D空间中的正方形,则它的面积绝对不能是任何东西,但其长度是平方的,证明它几乎是微不足道的。我们不需要断言以确保我们的世界是这样,我们只是利用现实的含义来防止我们的程序偏离轨道。
我几乎可以肯定是错的,但是我认为这可以帮助某个人,因此希望它可以对某个人有所帮助。
在Scala的上下文中,您将发现以下内容是最简单的定义。基本上,flatMap(或绑定)是“关联的”,并且存在一个标识。
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)
注:严格地说一个定义函数式编程单子是不一样的一个定义范畴论单子,这在圈中定义map
和flatten
。尽管在某些映射下它们是等效的。此演示文稿非常好:http : //www.slideshare.net/samthemonad/monad-presentation-scala-as-a-category
这个答案从一个有启发性的示例开始,贯穿该示例,得出一个monad的示例,并正式定义“ monad”。
考虑伪代码中的以下三个功能:
f(<x, messages>) := <x, messages "called f. ">
g(<x, messages>) := <x, messages "called g. ">
wrap(x) := <x, "">
f
接受表格<x, messages>
的有序对并返回有序对。它使第一项保持不变,并追加"called f. "
到第二项。与相同g
。
您可以组合这些函数并获得原始值,以及一个字符串,该字符串显示函数的调用顺序:
f(g(wrap(x)))
= f(g(<x, "">))
= f(<x, "called g. ">)
= <x, "called g. called f. ">
您不喜欢f
并g
负责将自己的日志消息附加到先前的日志记录信息的事实。(仅出于争辩的考虑,而不是附加字符串,f
并且g
必须在该对的第二项上执行复杂的逻辑。要在两个或更多不同的函数中重复该复杂的逻辑会很痛苦。)
您更喜欢编写简单的函数:
f(x) := <x, "called f. ">
g(x) := <x, "called g. ">
wrap(x) := <x, "">
但是,看看编写它们时会发生什么:
f(g(wrap(x)))
= f(g(<x, "">))
= f(<<x, "">, "called g. ">)
= <<<x, "">, "called g. ">, "called f. ">
问题在于,将一对传递给函数不会提供您想要的东西。但是,如果您可以将一对输入到函数中,该怎么办:
feed(f, feed(g, wrap(x)))
= feed(f, feed(g, <x, "">))
= feed(f, <x, "called g. ">)
= <x, "called g. called f. ">
读feed(f, m)
作“馈m
入f
”。为了养活一对<x, messages>
成功能f
是通过 x
进入f
,让<y, message>
出f
,并返回<y, messages message>
。
feed(f, <x, messages>) := let <y, message> = f(x)
in <y, messages message>
注意当您对函数执行三件事时会发生什么:
第一:如果包裹一个值,然后进料所得的一对成一个函数:
feed(f, wrap(x))
= feed(f, <x, "">)
= let <y, message> = f(x)
in <y, "" message>
= let <y, message> = <x, "called f. ">
in <y, "" message>
= <x, "" "called f. ">
= <x, "called f. ">
= f(x)
这与将值传递到函数中相同。
第二:如果您输入一对wrap
:
feed(wrap, <x, messages>)
= let <y, message> = wrap(x)
in <y, messages message>
= let <y, message> = <x, "">
in <y, messages message>
= <x, messages "">
= <x, messages>
那不会改变货币对。
第三:如果你定义一个函数,x
并反馈g(x)
到f
:
h(x) := feed(f, g(x))
并放入一对:
feed(h, <x, messages>)
= let <y, message> = h(x)
in <y, messages message>
= let <y, message> = feed(f, g(x))
in <y, messages message>
= let <y, message> = feed(f, <x, "called g. ">)
in <y, messages message>
= let <y, message> = let <z, msg> = f(x)
in <z, "called g. " msg>
in <y, messages message>
= let <y, message> = let <z, msg> = <x, "called f. ">
in <z, "called g. " msg>
in <y, messages message>
= let <y, message> = <x, "called g. " "called f. ">
in <y, messages message>
= <x, messages "called g. " "called f. ">
= feed(f, <x, messages "called g. ">)
= feed(f, feed(g, <x, messages>))
这与将货币对输入g
和将结果货币对输入相同f
。
您拥有大部分单子。现在,您只需要了解程序中的数据类型即可。
什么是价值类型<x, "called f. ">
?好吧,这取决于哪种类型的价值x
。如果x
是的类型t
,则您的对是“对t
和字符串对” 类型的值。叫那个类型M t
。
M
是类型构造函数:M
单独不引用类型,而是M _
在您用类型填充空白后引用类型。An M int
是一对int和一个字符串。An M string
是一对字符串和一个字符串。等等。
恭喜,您创建了一个monad!
正式地,您的monad是元组<M, feed, wrap>
。
一个monad是一个元组<M, feed, wrap>
,其中:
M
是类型构造函数。feed
接受(函数接受t
并返回M u
)和M t
并返回。M u
。wrap
取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
称为,(称为Haskell中的bind
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)
_ =<< [] = []
扩展=<<
串接++
所有列表[b]
由应用程序产生f x
一个Kleisli的箭头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, eta, _*>
终结者的
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
满足等价
mu . T(mu) = mu . mu : T . T . T -> T . T Associativity
join . map join = join . join :: f (f (f t)) -> f t
mu . T(eta) = mu . eta = id : T -> T Identity
join . map return = join . return = id :: f t -> f t
然后定义monad类型类
class Functor m => Monad m where
return :: t -> m t
join :: m (m t) -> m t
mu
选项monad 的规范实现:
instance Monad Maybe where
return = Just
join (Just m) = m
join Nothing = Nothing
该concat
功能
concat :: [[a]] -> [a]
concat (x : xs) = x ++ concat xs
concat [] = []
是join
清单monad的。
instance Monad [] where
return :: t -> [t]
return = (: [])
(=<<) :: (a -> [b]) -> ([a] -> [b])
(f =<<) = concat . map f
join
可以使用等价形式从扩展形式转换实现
mu = id* : T . T -> T
join = (id =<<) :: m (m t) -> m t
从反向转换mu
为扩展名形式为
f* = mu . T(f) : T(X) -> T(Y)
(f =<<) = join . map f :: m a -> m b
Philip Wadler:用于函数式编程的Monad
Simon L Peyton Jones,Philip Wadler:命令式函数式编程
Jonathan MD Hill,基思·克拉克(Keith Clarke):范畴论,范畴论单子及其与函数式编程 的关系的介绍 ´
Eugenio Moggi:计算和单子的概念
但是,为什么这么抽象的理论对编程有什么用呢?
答案很简单:作为计算机科学家,我们重视抽象!在设计软件组件的接口时,我们希望它尽可能少地揭示实现。我们希望能够用许多替代方案,同一“概念”的许多其他“实例”替代实现。当我们为许多程序库设计通用接口时,选择的接口具有多种实现就显得尤为重要。我们非常重视的是monad概念的普遍性,这是因为类别理论是如此抽象,以至于其概念对于编程非常有用。
因此,我们在下面介绍的monad的泛化也与范畴论有着密切的联系,这几乎是令人惊讶的。但是我们强调我们的目的是非常实际的:不是“实现类别理论”,而是找到一种更通用的构造组合器库的方法。数学家们已经为我们完成了许多工作,这仅仅是我们的幸运!
从要概括单子到箭头由约翰·休斯
世界需要的是另一篇有关monad的博客文章,但我认为这对于确定野外现有的monad很有用。
上面的分形叫做Sierpinski三角形,这是我能记得绘制的唯一分形。分形是自相似的结构,如上述三角形,其中各部分与整体相似(在这种情况下,比例只是父三角形的一半)。
单子是分形的。给定一元数据结构,其值可以组成另一个数据结构值。这就是为什么它对编程有用的原因,也是为什么它在许多情况下都发生的原因。
http://code.google.com/p/monad-tutorial/正在进行中,可以确切解决这个问题。
让下面的“ {| a |m}
”代表一些单子数据。宣传的数据类型a
:
(I got an a!)
/
{| a |m}
函数f
知道如何创建一个monad(如果只有一个)a
:
(Hi f! What should I be?)
/
(You?. Oh, you'll be /
that data there.) /
/ / (I got a b.)
| -------------- |
| / |
f a |
|--later-> {| b |m}
在这里,我们看到函数f
试图求一个单子,但被斥责了。
(Hmm, how do I get that a?)
o (Get lost buddy.
o Wrong type.)
o /
f {| a |m}
Funtion f
找到了一种使用提取的a
方法>>=
。
(Muaahaha. How you
like me now!?)
(Better.) \
| (Give me that a.)
(Fine, well ok.) |
\ |
{| a |m} >>= f
鲜为人知的是f
,单子和>>=
正在勾结。
(Yah got an a for me?)
(Yeah, but hey |
listen. I got |
something to |
tell you first |
...) \ /
| /
{| a |m} >>= f
但是他们实际上在谈论什么呢?好吧,这取决于单子。仅以抽象方式交谈具有有限的用途;您必须对特定的单子有一定的经验才能充实理解。
例如,数据类型Maybe
data Maybe a = Nothing | Just a
有一个monad实例,其行为类似于以下内容...
其中,如果是这样 Just a
(Yah what is it?)
(... hm? Oh, |
forget about it. |
Hey a, yr up.) |
\ |
(Evaluation \ |
time already? \ |
Hows my hair?) | |
| / |
| (It's |
| fine.) /
| / /
{| a |m} >>= f
但是对于 Nothing
(Yah what is it?)
(... There |
is no a. ) |
| (No a?)
(No a.) |
| (Ok, I'll deal
| with this.)
\ |
\ (Hey f, get lost.)
\ | ( Where's my a?
\ | I evaluate a)
\ (Not any more |
\ you don't. |
| We're returning
| Nothing.) /
| | /
| | /
| | /
{| a |m} >>= f (I got a b.)
| (This is \
| such a \
| sham.) o o \
| o|
|--later-> {| b |m}
因此,如果Mayad monad实际上包含a
它要通告的内容,则可以让计算继续进行,但如果不包含它,则中止计算。结果仍然是单子数据,尽管不是的输出f
。因此,可能使用Maybe monad表示失败的上下文。
不同的单子的行为不同。列表是具有单例实例的其他类型的数据。它们的行为如下:
(Ok, here's your a. Well, its
a bunch of them, actually.)
|
| (Thanks, no problem. Ok
| f, here you go, an a.)
| |
| | (Thank's. See
| | you later.)
| (Whoa. Hold up f, |
| I got another |
| a for you.) |
| | (What? No, sorry.
| | Can't do it. I
| | have my hands full
| | with all these "b"
| | I just made.)
| (I'll hold those, |
| you take this, and /
| come back for more /
| when you're done /
| and we'll do it /
| again.) /
\ | ( Uhhh. All right.)
\ | /
\ \ /
{| a |m} >>= f
在这种情况下,该函数知道如何根据其输入创建列表,但不知道如何处理额外的输入和额外的列表。bind >>=
,f
通过组合多个输出来提供帮助。我包含此示例,以说明尽管>>=
它负责提取a
,但它也可以访问的最终绑定输出f
。实际上,a
除非知道最终输出具有相同类型的上下文,否则它将永远不会提取任何内容。
还有其他monad用于表示不同的上下文。这里是一些其他特征。该IO
单子实际上并没有一个a
,但它知道一个家伙,会得到a
你。该State st
monad有一个秘密藏身之处,即使它刚要了st
,它也会传递到f
桌子底下。该单子类似,虽然它只是让看。f
a
Reader r
State st
f
r
所有这些的意义在于,任何本身声明为Monad的数据类型都在声明从monad提取值的某种上下文。从这一切中获得最大收益?好吧,它很容易在某种上下文中进行计算。但是,当将多个上下文加载计算串在一起时,可能会变得混乱。monad操作负责解决上下文的交互,因此程序员不必这样做。
请注意,使用>>=
减轻了的部分自治,从而减轻了混乱f
。也就是说,例如在上述情况Nothing
下,f
不再需要决定在的情况下该怎么做Nothing
;它编码为>>=
。这是权衡。如果有必要f
决定在情况下该怎么做Nothing
,则f
应该是from Maybe a
到函数Maybe b
。在这种情况下,Maybe
成为单子无关紧要。
但是请注意,有时数据类型不会导出它的构造函数(在IO上看),并且如果我们要使用广告值,则别无选择,只能使用它的monadic接口。
monad是用于封装状态变化的对象的东西。在其他不允许您具有可修改状态的语言(例如,Haskell)中最经常遇到这种情况。
例如文件I / O。
您将能够使用monad进行文件I / O,以将不断变化的状态性质与仅使用Monad的代码隔离开。Monad内部的代码可以有效地忽略Monad外部世界的变化状态-这使您更容易推断程序的整体效果。