为什么monads? 它如何解决副作用?

我正在学习Haskell并试图理解Monads。 我有2个问题。

据我所知,Monad只是另一个types类,它声明了与“容器”内的数据进行交互的方法,包括Maybes,Lists和IOs。 用一个概念来实现这3个东西似乎是聪明和干净的,但真正的重点是在一系列函数,容器和副作用中可以有一个干净的error handling。 这是一个正确的解释?

其次,副作用的问题究竟如何解决? 用这个容器的概念,语言本质上是说容器内的任何东西都是非确定性的(如I / O)。 因为列表和IO都是容器,所以列表与IO是等价的,即使列表中的值对我来说也是相当确定的。 那么什么是确定性的,什么是副作用呢? 我不能把我的头围绕一个基本的值是确定性的想法,直到你把它放在一个容器中(这个容器没有什么特别的地方,而且它旁边还有一些其他值,例如Nothing),现在可以是随机的。

有人可以解释一下,Haskell如何通过input和输出改变状态? 我在这里看不到魔法。

关键是在一系列函数,容器和副作用中可以有干净的error handling。 这是一个正确的解释?

不是真的。 你已经提到了许多人们在试图解释单子时引用的概念,包括副作用,error handling和非确定性,但是听起来你已经意识到所有这些概念都适用于所有单子。 但是你提到的一个概念是: 链接

这有两种不同的口味,所以我会用两种不同的方式来解释:一种没有副作用,另一种有副作用。

无副作用:

以下面的例子:

 addM :: (Monad m, Num a) => ma -> ma -> ma addM ma mb = do a <- ma b <- mb return (a + b) 

这个函数增加了两个数字,它们被包裹在一些monad中。 哪个monad? 没关系! 在所有情况下,这个特殊的语法脱糖如下:

 addM ma mb = ma >>= \a -> mb >>= \b -> return (a + b) 

…或者,运算符优先级明确:

 ma >>= (\a -> mb >>= (\b -> return (a + b))) 

现在你可以真正看到这是一个小函数链,全部组合在一起,它的行为将取决于如何为每个monad定义>>=return 。 如果您熟悉面向对象语言中的多态性,这本质上是一样的:一个与多个实现的通用接口。 它比平均的面向对象接口略微更有意思,因为接口代表了计算策略,而不是动物,形状或其他东西。

好的,我们来看一些addM如何在不同单子addMperformance的例子。 Identity monad是一个体面的开始,因为它的定义是微不足道的:

 instance Monad Identity where return a = Identity a -- create an Identity value (Identity a) >>= f = fa -- apply f to a 

那么当我们说:

 addM (Identity 1) (Identity 2) 

一步一步展开:

 (Identity 1) >>= (\a -> (Identity 2) >>= (\b -> return (a + b))) (\a -> (Identity 2) >>= (\b -> return (a + b)) 1 (Identity 2) >>= (\b -> return (1 + b)) (\b -> return (1 + b)) 2 return (1 + 2) Identity 3 

大。 现在,既然你提到了干净的error handling,让我们来看看Maybe monad。 它的定义只比Identity稍微复杂:

 instance Monad Maybe where return a = Just a -- same as Identity monad! (Just a) >>= f = fa -- same as Identity monad again! Nothing >>= _ = Nothing -- the only real difference from Identity 

所以你可以想象,如果我们说addM (Just 1) (Just 2)我们会得到Just 3 。 但是对于咧嘴addM Nothing (Just 1) ,让我们展开addM Nothing (Just 1)

 Nothing >>= (\a -> (Just 1) >>= (\b -> return (a + b))) Nothing 

或者相反, addM (Just 1) Nothing

 (Just 1) >>= (\a -> Nothing >>= (\b -> return (a + b))) (\a -> Nothing >>= (\b -> return (a + b)) 1 Nothing >>= (\b -> return (1 + b)) Nothing 

所以Maybe monad的定义>>=被调整来解释失败。 当使用>>=将一个函数应用于一个Maybe值时,你会得到你所期望的。

好吧,所以你提到了非确定性。 是的,列表monad可以被认为是在某种意义上build模非确定性…这有点奇怪,但是将列表想象成代表可选的替代值: [1, 2, 3]不是集合,它是单一的非确定性数字可以是一个,两个或三个。 这听起来很愚蠢,但是当你考虑如何为列表定义>>=时,它开始变得有意义:它将给定的函数应用于每个可能的值。 所以addM [1, 2] [3, 4]实际上是要计算这两个非确定性值的所有可能的和addM [1, 2] [3, 4] [4, 5, 5, 6]

好,现在解决你的第二个问题

副作用:

假设您将addM应用于IO monad中的两个值,如:

 addM (return 1 :: IO Int) (return 2 :: IO Int) 

你没有得到任何特别的东西,在IO monad中只有3个。 addM不读取或写入任何可变的状态,所以这是没有乐趣。 StateST单体也一样。 不好玩。 所以让我们使用一个不同的function:

 fireTheMissiles :: IO Int -- returns the number of casualties 

每次发射导弹显然这个世界是不一样的。 显然。 现在让我们假设你正在尝试写一些完全无害的,无副作用的非导弹启动代码。 也许你会再次尝试添加两个数字,但是这一次没有任何单子飞来飞去:

 add :: Num a => a -> a -> a add ab = a + b 

一下子你的手滑了,你不小心打错了:

 add ab = a + b + fireTheMissiles 

一个诚实的错误,真的。 钥匙是如此接近。 幸运的是,因为fireTheMissiles的types是IO Int而不是简单的Int ,编译器能够避免灾难。

好吧,完全是人为的例子,但重点是,在IOST和朋友的情况下,types系统保持一些特定的上下文的影响。 它不会奇迹般地消除副作用,使得代码在本质上不透明,但是它在编译时清楚地表明了这种效应所限制的范围。

所以回到原来的观点:这与链接或函数组合有什么关系? 那么,在这种情况下,这只是expression一系列效果的方便方式:

 fireTheMissilesTwice :: IO () fireTheMissilesTwice = do a <- fireTheMissiles print a b <- fireTheMissiles print b 

概要:

单子表示链式计算的一些策略。 Identity的政策是纯粹的function组合, Maybe政策是function组合与失败的传播, IO的政策是不纯的function组成等等。

让我先指出一下优秀的“ 你可以发明单子 ”的文章。 它演示了Monad结构如何在编写程序时自然显现。 但是这个教程没有提到IO ,所以我会在这里扩展这个方法。

让我们从你可能已经看到的 – 容器monad开始吧。 假设我们有:

 f, g :: Int -> [Int] 

看待这个问题的一个方法是,它为我们提供了一些可能的输出,用于每一个可能的input。 如果我们需要所有可能的输出来组合这两个函数呢? 给予所有的可能性,我们可以通过一个接一个地应用函数来获得吗?

那么,有一个function:

 fg x = concatMap g $ fx 

如果我们把这个更一般化,我们可以得到

 fg x = fx >>= g xs >>= f = concatMap f xs return x = [x] 

我们为什么要这样包装呢? 那么,主要使用>>=来编写我们的程序,并return给我们一些不错的属性 – 例如,我们可以肯定,“忘记”解决scheme相对困难。 我们明确地必须重新引入它,比如说增加另一个函数skip 。 而且我们现在有一个monad,可以使用monad库中的所有combinator!

现在,让我们跳到你更棘手的例子。 假设这两个function是“副作用”的。 这不是非确定性的,它只是意味着从理论上讲,整个世界既是它们的input(因为它可以影响它们),也是它们的输出(因为函数可以影响它)。 所以我们得到这样的东西:

 f, g :: Int -> RealWorld# -> (Int, RealWorld#) 

如果我们现在想让f得到留下的世界,我们会写:

 fg x rw = let (y, rw') = fx rw (r, rw'') = gy rw' in (r, rw'') 

或者泛化:

 fg x = fx >>= g x >>= f = \rw -> let (y, rw') = x rw (r, rw'') = fy rw' in (r, rw'') return x = \rw -> (x, rw) 

现在,如果用户只能使用>>=return和一些预定义的IO值,我们再次得到一个不错的属性:用户将永远不会真正看到 RealWorld#传递! 这是一件非常好的事情,因为您对getLine从中获取数据的细节并不感兴趣。 再次,我们从monad库中获得所有不错的高级函数。

所以重要的事情要拿走:

  1. monad捕获代码中的常见模式,比如“始终将容器A的所有元素传递给容器B”或“传递这个真实世界标签”。 通常情况下,一旦你意识到在你的程序中有一个monad,复杂的事情就变成了正确的monad组合器的应用程序。

  2. monad允许你完全隐藏用户的实现。 这是一个很好的封装机制,无论是为了你自己的内部状态,还是IO如何设法以相对安全的方式将非纯度压缩成一个纯粹的程序。


附录

如果还有RealWorld#我开始时一样在RealWorld#RealWorld#头:当所有的monad抽象被移除后,显然会有更多的魔法发生。 那么编译器就会利用只有一个“现实世界”的事实。 这是好消息和坏消息:

  1. 因此,编译器必须保证函数之间的执行顺序(这就是我们之后的!)

  2. 但是这也意味着实际上通过现实世界并不是必须的,因为只有一个我们可能的意思:当函数执行时是最新的那个!

底线是一旦执行顺序被修复, RealWorld#就会被优化。 因此,使用IO monad的程序实际上具有零运行时间的开销。 另外请注意,使用RealWorld#显然只有一种可能的方式来放置IO – 但它恰好是GHC内部使用的一种。 monads的好处在于用户真的不需要知道。

你可以看到一个给定的monad m作为行为的集合/家族(或领域,领域等)(想象一个C语句)。 monad m定义了它的动作可能具有的(side-)效果types:

  • []你可以定义行动,可以在不同的“独立的平行世界”分叉他们的执行;
  • Either Foo可以定义可能失败的动作types为Foo错误;
  • 使用IO你可以定义在“外部世界”(访问文件,networking,启动进程,执行HTTP GET …)有副作用的动作。
  • 你可以有一个monad的效果是“随机性”(参见软件包MonadRandom );
  • 你可以定义一个monad,它的行为可以在游戏中移动(比如说象棋,Go …),接受来自对手的移动,但不能写入你的文件系统或其他东西。

概要

如果m是monad,则ma是产生typesa的结果/输出的动作

>>>>=操作符被用来创build更简单的更复杂的操作:

  • a >> b是行动a然后行动b的macros观行为;
  • a >> a行动a再行动a ;
  • >>=第二个动作可以依赖于第一个的输出。

动作是什么, 动作是什么, 动作又动作的确切意义取决于monad:每个monad都定义了一个具有某些特征/效果的命令式子语言。

简单的测序( >>

让我们说有一个给定的单子M和一些行动 incrementCounterdecrementCounterreadCounter

 instance M Monad where ... -- Modify the counter and do not produce any result: incrementCounter :: M () decrementCounter :: M () -- Get the current value of the counter readCounter :: M Integer 

现在我们想要做一些有趣的事情。 我们要做的第一件事就是对它们进行sorting。 正如在说C,我们希望能够做到:

 // This is C: counter++; counter++; 

我们定义一个“测序算子” >> 。 使用这个运算符我们可以写:

 incrementCounter >> incrementCounter 

什么是“增量计数器”增量计数器的types?

  1. 这是一个由C中两个较小的动作组成的动作,你可以从primefaces语句中编写组合语句:

     // This is a macro statement made of several statements { counter++; counter++; } // and we can use it anywhere we may use a statement: if (condition) { counter++; counter++; } 
  2. 它可以具有与其子作用相同的效果;

  3. 它不会产生任何输出/结果。

所以我们想让incrementCounter >> incrementCounter成为M ()types的一个(macros)动作,它具有相同types的可能的效果,但是没有任何输出。

更一般地说,给出两个动作:

 action1 :: M a action2 :: M b 

我们定义一个a >> b作为通过 (在我们的行动领域的任何方式) bb获得的macros观行为,并产生第二个行动的执行结果作为输出。 >>的types是:

 (>>) :: M a -> M b -> M b 

或者更一般地说:

 (>>) :: (Monad m) => ma -> mb -> mb 

我们可以从更简单的angular度定义更大的操作顺序:

  action1 >> action2 >> action3 >> action4 

input和输出( >>=

我们希望能够增加其他的东西,一次一个:

 incrementBy 5 

我们想要在我们的行动中提供一些input,为了做到这一点,我们定义一个函数incrementBy采取一个Int并产生一个行动:

 incrementBy :: Int -> M () 

现在我们可以写下如下的东西:

 incrementCounter >> readCounter >> incrementBy 5 

但是我们没有办法将readCounter的输出提供给incrementBy 。 为了做到这一点,我们需要一个稍微更强大的测序算子版本。 >>=操作符可以将给定操作的输出作为input提供给下一个操作。 我们可以写:

 readCounter >>= incrementBy 

这是一个执行readCounter动作的动作,在incrementBy函数中提供其输出,然后执行结果动作。

>>=的types是:

 (>>=) :: Monad m => ma -> (a -> mb) -> mb 

一个(部分)例子

假设我有一个Prompt monad,它只能显示信息(文本)给用户并向用户提供信息:

 -- We don't have access to the internal structure of the Prompt monad module Prompt (Prompt(), echo, prompt) where -- Opaque data Prompt a = ... instance Monad Prompt where ... -- Display a line to the CLI: echo :: String -> Prompt () -- Ask a question to the user: prompt :: String -> Prompt String 

让我们尝试定义一个提示问题的布尔promptBoolean message动作,并产生一个布尔值。

我们使用提示符(message ++ "[y/n]")操作并将其输出提供给函数f

  • f "y"应该是一个什么也不做,只能产生True行为;

  • f "n"应该是一个什么都不做的行为,只能产生False作为输出;

  • 其他任何事情都应该重新开始行动(再次行动);

promptBoolean看起来像这样:

  -- Incomplete version, some bits are missing: promptBoolean :: String -> M Boolean promptBoolean message = prompt (message ++ "[y/n]") >>= f where f result = if result == "y" then ???? -- We need here an action which does nothing but produce `True` as output else if result=="n" then ???? -- We need here an action which does nothing but produce `False` as output else echo "Input not recognised, try again." >> promptBoolean 

生成无效的值( return

为了在我们的promptBoolean函数中填充缺失的位,我们需要一种方法来表示没有任何副作用但是只输出给定值的假动作:

 -- "return 5" is an action which does nothing but outputs 5 return :: (Monad m) => a -> ma 

现在我们可以写出promptBoolean函数了:

 promptBoolean :: String -> Prompt Boolean promptBoolean message :: prompt (message ++ "[y/n]") >>= f where f result = if result=="y" then return True else if result=="n" then return False else echo "Input not recognised, try again." >> promptBoolean message 

通过组合这两个简单的动作( promptBooleanecho ),我们可以定义用户和程序之间的任何对话(程序的动作是确定性的,因为我们的monad没有“随机效应”)。

 promptInt :: String -> M Int promptInt = ... -- similar -- Classic "guess a number game/dialogue" guess :: Int -> m() guess n = promptInt "Guess:" m -> f where fm = if m == n then echo "Found" else (if m > n then echo "Too big" then echo "Too small") >> guess n 

monad的操作

Monad是一组可以用return>>=操作符组成的操作:

  • >>=为行动组成;

  • return产生一个没有任何(side-)效果的值。

这两个运算符是定义Monad所需的最小运算符。

在Haskell中,也需要>>运算符,但它实际上可以从>>=派生:

 (>>): Monad m => ma -> mb -> mb a >> b = a >>= f where fx = b 

在Haskell中,还需要一个额外的fail操作符,但这实际上是一个黑客( 将来可能会从Monad中删除 )。

这是Monad的Haskell定义:

 class Monad m where return :: ma (>>=) :: ma -> (a -> mb) -> mb (>>) :: ma -> mb -> mb -- can be derives from (>>=) fail :: String -> ma -- mostly a hack 

行动是一stream的

关于monads的一个伟大的事情是行动是一stream的。 你可以把它们放在一个variables中,你可以定义一个函数,它把操作当作input,并产生一些其他的操作作为输出。 例如,我们可以定义一个while运算符:

 -- while xy : does action y while action x output True while :: (Monad m) => m Boolean -> ma -> m () while xy = x >>= f where f True = y >> while xy f False = return () 

概要

Monad是一些域中的一组操作 。 monad / domain定义了可能的“效果”types。 >>>>=运算符表示动作的顺序,单子expression式可以用来表示(函数式)Haskell程序中的任何一种“命令(子)程序”。

伟大的事情是:

  • 你可以devise你自己的Monad ,它支持你想要的function和效果

    • 请参阅Prompt “仅对话子程序”的示例

    • 参见Rand “仅采样子程序”的例子;

  • 你可以编写自己的控制结构( whilethrowcatch或更奇特的)作为function采取行动,并以某种方式组成它们来产生更大的macros观行动。

MonadRandom

MonadRandom软件包是理解MonadRandom一个好方法。 Rand monad是由输出可以是随机的行为(效果是随机的)。 这个monad中的一个动作是某种随机variables(或者更准确地说是一个抽样过程):

  -- Sample an Int from some distribution action :: Rand Int 

使用Rand来做一些采样/随机algorithm是非常有趣的,因为你有随机variables作为第一类值:

 -- Estimate mean by sampling nsamples times the random variable x sampleMean :: Real a => Int -> ma -> ma sampleMean nx = ... 

在这个设置中, Preludesequencefunction,

  sequence :: Monad m => [ma] -> m [a] 

  sequence :: [Rand a] -> Rand [a] 

它创build一个随机variables,通过从随机variables列表中独立抽样获得。

有一件事情经常帮助我理解事物的本质,就是以最微不足道的方式去研究它。 那样,我就不会被可能不相关的概念分心。 考虑到这一点,我认为了解Monad Monad的本质可能是有帮助的,因为Monad可能是最简单的实现(我认为)。

Identity Monad有趣的是什么? 我认为这是它允许我expression在由其他expression式定义的上下文中评估expression式的想法。 对我来说,这是我所遇到的每一个Monad的本质(到目前为止)。

如果你在学习Haskell之前已经接触过“主stream”编程语言(就像我做过的那样),那么这看起来并不是很有趣。 毕竟,在一种主stream的编程语言中,语句按顺序依次执行(当然,除了控制stream结构之外)。 当然,我们可以假定每个语句都是在所有先前执行的语句的上下文中进行评估的,而且那些以前执行的语句可能会改变环境和当前正在执行的语句的行为。

所有这些在Haskell这样的function性,懒惰的语言中几乎都是一个外国概念。 在Haskell中计算的顺序是明确的,但有时很难预测,甚至更难控制。 而对于许多种问题,这很好。 但是,如果没有一些方便的方法来在程序中计算之间build立一个隐含的顺序和上下文,其他types的问题(例如IO)就很难解决。

就副作用而言,具体而言,通常他们可以通过一个Monad进行简单的状态传递,这在纯粹的function语言中是完全合法的。 然而,一些Monad似乎不属于这种性质。 Monad,如IO Monad或ST monad字面上会执行副作用。 有很多方法可以思考这个问题,但我想到的一个方法就是,因为我的计算必须存在于一个没有副作用的世界里,Monad可能不会。 因此,Monad可以自由地为我的计算build立一个上下文,以执行其他计算所定义的副作用。

最后,我必须声明,我绝对不是哈斯克尔的专家。 因此,请理解我所说的一切都是我自己对这个问题的想法,稍后当我更充分地理解Monad时,我很可能会拒绝他们。

关于IO单子有三个主要观察:

1)你不能从中获得价值。 像Maybe这样的其他types可能允许提取值,但monad类接口本身和IO数据types都不允许。

2)“内部” IO不仅是真正的价值,也是“真实世界”的东西。 该虚拟值用于强制types系统的动作链接:如果有两个独立的计算,使用>>=使第二个计算依赖于第一个计算。

3)假设像random :: () -> Int这样的非确定性事物,这在Haskell中是不允许的。 如果您将签名更改为random :: Blubb -> (Blubb, Int) ,那么允许您确认没有人可以使用Blubb两次:因为在这种情况下,所有input都是“不同的”,这是没有问题的产出也是不同的。

现在我们可以使用这个事实1):没有人能从IO获得某些东西,所以我们可以使用隐藏在IORealWord dummy来充当Blubb 。 整个应用程序中只有一个IO (我们从main获得),并且需要正确的顺序处理,正如我们在2)中所看到的那样。 问题解决了。

问题的关键是如何在一系列函数,容器和副作用中处理干净的错误

或多或less。

副作用问题究竟如何解决?

I / O monad中的一个值,即IO atypes中的IO a ,应该被解释为一个程序。 IO值的p >> q可以被解释为将两个程序组合成一个首先执行p ,然后q的操作符。 其他monad运营商也有类似的解释。 通过给名字main分配一个程序,你可以向编译器声明这是必须由其输出目标代码执行的程序。

至于单子列表,除了非常抽象的math意义之外,它与I / O单子并不相关。 IO monad给出了带有副作用的确定性计算,而list monad给出了非确定性(但不是随机的)回溯search​​,有点类似于Prolog的操作方式。

用这个容器的概念,这个语言实质上是说容器内的任何东西都是不确定的

哈斯克尔是确定性的。 如果你要求整数加2 + 2,你总会得到4。

“非确定性”只是一种隐喻,一种思维方式。 一切都是确定性的。 如果你有这个代码:

 do x <- [4,5] y <- [0,1] return (x+y) 

它大致相当于Python代码

  l = [] for x in [4,5]: for y in [0,1]: l.append(x+y) 

你在这里看到不确定性? 不,这是确定性的列表结构。 运行两次,你会得到相同的顺序相同的数字。

你可以这样描述:从[4,5]中select任意的x。 从[0,1]中select任意的y。 返回x + y。 收集所有可能的结果。

这种方式似乎涉及非确定性,但它只是一个嵌套的循环(列表理解)。 这里没有“真正的”不确定性,它是通过检查所有的可能性来模拟的。 非确定性是一种幻觉。 代码似乎只是非确定性的。

这个代码使用状态monad:

 do put 0 x <- get put (x+2) y <- get return (y+3) 

给5,似乎涉及改变状态。 与列表一样,这是一个幻想。 没有变化(如命令式语言)。 一切都是不可改变的。

你可以这样描述代码:把0赋给一个variables。 读取一个variables的值为x。 把(x + 2)放到variables中。 将variables读入y,然后返回y + 3。

这种方式似乎涉及到状态,但它只是构成传递附加参数的函数。 这里没有“真正的”可变性,它是通过构图来模拟的。 可变性是一种幻觉。 代码似乎只使用它。

哈斯克尔这样做:你有function

  a -> s -> (b,s) 

这个函数取得状态的旧值并返回新的值。 它不涉及可变性或variablesvariables。 这是math意义上的函数。

例如,function“put”取得新的状态值,忽略当前的状态并返回新的状态:

  put x _ = ((), x) 

就像你可以编写两个正常的函数一样

  a -> b b -> c 

  a -> c 

使用(。)运算符可以组成“状态”变换器

  a -> s -> (b,s) b -> s -> (c,s) 

成一个单一的function

  a -> s -> (c,s) 

尝试自己写作文作业。 这是真正发生的事情,没有“副作用”只是传递函数的参数。