为什么在Haskell中将副作用建模为monad?


175

任何人都可以对为什么Haskell中的不纯计算被建模为monad给出一些指示吗?

我的意思是monad只是一个具有4种操作的接口,所以在其中建模副作用的原因是什么?


15
Monad仅定义两个操作。
达里奥

3
但是退货和失败呢?((>>)和(>> =)除外)
bodacydo

55
这两个操作是return(>>=)x >> yx >>= \\_ -> y(即忽略第一个参数的结果)相同。我们不谈论fail
porges

2
@Porges为什么不谈论失败?它在一定程度上,即有用也许,分析器等
替代

16
@monadic:由于历史事故failMonad上课;它确实属于MonadPlus。请注意,其默认定义是不安全的。
JB。

Answers:


294

假设一个函数有副作用。如果我们将其产生的所有效果作为输入和输出参数,则该函数对外界是纯净的。

因此,对于不纯净的功能

f' :: Int -> Int

我们将RealWorld加入考虑范围

f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.

然后又f是纯净的。我们定义了一个参数化的数据类型type IO a = RealWorld -> (a, RealWorld),因此我们不需要多次键入RealWorld,而只需编写

f :: Int -> IO Int

对于程序员来说,直接处理RealWorld太危险了-特别是,如果程序员接触到RealWorld类型的值,他们可能会尝试复制它,这基本上是不可能的。(例如,想尝试复制整个文件系统。您将它放在哪里?)因此,我们对IO的定义也封装了整个世界的状态。

“不纯”功能的组成

如果我们不能将它们链接在一起,那么这些不纯函数就没有用了。考虑

getLine     :: IO String            ~            RealWorld -> (String, RealWorld)
getContents :: String -> IO String  ~  String -> RealWorld -> (String, RealWorld)
putStrLn    :: String -> IO ()      ~  String -> RealWorld -> ((),     RealWorld)

我们想

  • 从控制台获取文件名,
  • 读取该文件,然后
  • 将文件的内容打印到控制台。

如果我们可以进入现实世界中的国家,该怎么办?

printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
                       (contents, world2) = (getContents filename) world1 
                   in  (putStrLn contents) world2 -- results in ((), world3)

我们在这里看到一个模式。这些函数的调用方式如下:

...
(<result-of-f>, worldY) = f               worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...

因此,我们可以定义一个运算符~~~来绑定它们:

(~~~) :: (IO b) -> (b -> IO c) -> IO c

(~~~) ::      (RealWorld -> (b,   RealWorld))
      ->                    (b -> RealWorld -> (c, RealWorld))
      ->      (RealWorld                    -> (c, RealWorld))
(f ~~~ g) worldX = let (resF, worldY) = f worldX
                   in g resF worldY

然后我们可以简单地写

printFile = getLine ~~~ getContents ~~~ putStrLn

而不接触现实世界。

“净化”

现在假设我们也要使文件内容大写。大写是一个纯粹的功能

upperCase :: String -> String

但是要使其进入现实世界,它必须返回IO String。提升这样的功能很容易:

impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)

可以概括为:

impurify :: a -> IO a

impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)

这样impureUpperCase = impurify . upperCase,我们可以写

printUpperCaseFile = 
    getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn

(注意:通常我们写getLine ~~~ getContents ~~~ (putStrLn . upperCase)

我们一直在和单子一起工作

现在让我们看看我们做了什么:

  1. 我们定义了一个(~~~) :: IO b -> (b -> IO c) -> IO c将两个不纯函数链接在一起的运算符
  2. 我们定义了一个impurify :: a -> IO a将纯值转换为不纯值的函数。

现在我们做的鉴定(>>=) = (~~~)return = impurify,并看到了吗?我们有一个单子。


技术说明

为了确保它确实是monad,仍然需要检查一些公理:

  1. return a >>= f = f a

     impurify a                =  (\world -> (a, world))
    (impurify a ~~~ f) worldX  =  let (resF, worldY) = (\world -> (a, world )) worldX 
                                  in f resF worldY
                               =  let (resF, worldY) =            (a, worldX)       
                                  in f resF worldY
                               =  f a worldX
    
  2. f >>= return = f

    (f ~~~ impurify) worldX  =  let (resF, worldY) = f worldX 
                                in impurify resF worldY
                             =  let (resF, worldY) = f worldX      
                                in (resF, worldY)
                             =  f worldX
    
  3. f >>= (\x -> g x >>= h) = (f >>= g) >>= h

    留做运动。


5
+1,但我想指出,这专门涵盖了IO情况。 blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html非常相似,但是可以概括RealWorld为...好吧,您将看到。
短暂

4
请注意,这种解释不能真正适用于Haskell IO,因为后者支持交互,并发和不确定性。请参阅我对这个问题的回答以获得更多指示。
Conal

2
@Conal GHC实际上确实以IO这种方式实现,但RealWorld实际上并不代表现实世界,它只是使操作井然有序的一个标记(“魔术”RealWorld是GHC Haskell唯一的唯一性类型)
Jeremy List

2
@JeremyList据我了解,GHCIO通过将此表示形式与非标准的编译器魔术结合起来实现的(使人联想到Ken Thompson著名的C编译器病毒)。对于其他类型,事实与通常的Haskell语义一起存在于源代码中。
康那,2015年

1
@Clonal我的评论归因于我阅读了GHC源代码的相关部分。
杰里米·

43

任何人都可以对为什么Haskell中的不纯计算被建模为monad给出一些指示吗?

这个问题存在广泛的误解。杂质和Monad是独立的概念。杂质不是Monad的模型。相反,有几种数据类型,例如IO,表示命令式计算。对于其中一些类型,其接口的一小部分对应于称为“ Monad”的接口模式。此外,尽管存在关于的含义的通常被称为故事,但尚无已知的纯粹的/功能性的/解释性的解释IO(考虑到的“罪恶箱”的目的,不可能有一个IO)。这个故事不能如实描述,因为World -> (a, World)IO aIOIO支持并发和不确定性。当进行确定性计算以允许与计算机进行中间交互时,该故事甚至不起作用。

有关更多说明,请参见此答案

编辑:在重新阅读问题时,我认为我的答案不太正确。正如问题所表明的那样,命令式计算模型的确经常成为单子。质问者可能不会真正假设单调性能够实现命令式计算的建模。


1
@KennyTM:但是RealWorld,正如文档所说,这“非常神奇”。它是表示运行时系统正在执行的操作的令牌,实际上对现实世界没有任何意义。您甚至不能想出一个新的线程来做“线程”,而不必做额外的欺骗。天真的方法只会创建一个单一的阻止动作,并且在何时运行会产生很大的不确定性。
CA McCann

4
另外,我认为单子本质上必不可少的。如果函子表示某个结构,其中嵌入了值,则monad实例意味着您可以基于这些值构建和展平新层。因此,无论您指定给函子的单个层什么意思,单键表示您都可以创建无限多个层,并具有从上到下的严格因果关系。特定实例可能没有内在的命令结构,但Monad总的来说确实如此。
CA McCann

3
Monad一般”是指forall m. Monad m => ...,即在任意实例上工作。您可以使用任意monad执行的操作几乎与您执行的操作完全相同IO:接收不透明的基元(分别作为函数参数,或从库中获取),使用构造无操作return,或使用不可逆方式转换值(>>=)。在任意monad中进行编程的本质是生成一系列不可撤销的动作:“先执行X,然后执行Y,然后...”。听起来对我来说非常重要!
CA McCann

2
不,您仍然在这里遗漏我的观点。当然,对于任何这些特定类型,您都不会使用这种心态,因为它们具有清晰,有意义的结构。当我说“任意单子”时,我的意思是“你没有选择哪一个”。这里的角度是从量词内部进行的,因此认为m存在性可能会更有帮助。此外,我的“解释”是对法律的重新表述;“ do X”语句的列表恰好是通过创建的未知结构上的自由monoid (>>=);而monad法则只是关于endofunctor组成的monoid法则。
加州麦肯

3
简而言之,所有monad一起描述的最大下限是对未来的盲目,毫无意义的前进。IO这是一个病理情况,恰恰是因为它几乎没有提供任何其他功能。在特定情况下,类型可能揭示更多的结构,因此具有实际意义;但是在其他方面,基于法律,一元论的基本特性与原样清晰地相反IO。如果不导出构造函数,穷举枚举原始操作或类似操作,这种情况就绝望了。
加利福尼亚州麦肯

13

据我了解,一个叫Eugenio Moggi的人首先注意到,以前模糊的数学结构“ monad”可用于对计算机语言中的副作用进行建模,从而使用Lambda演算来指定其语义。在开发Haskell时,可以采用多种方法对不正确的计算进行建模(有关更多详细信息,请参见Simon Peyton Jones的“头发衬衫”文章),但是当Phil Wadler介绍monads时,很快就很明显这就是答案。剩下的就是历史。


3
不完全的。众所周知,一个monad可以在很长的时间内对解释进行建模(至少从“ Topoi:逻辑的分类分析”开始)。另一方面,除非强类型化函数,否则无法清楚地表达monad的类型。语言来了,然后莫吉将两个和两个放在一起
诺曼

1
如果monads是根据map wrap和unwrap进行定义的,则它们可能更容易理解,return是wrap的同义词。
aoeu256 '19

9

任何人都可以对为什么Haskell中的不纯计算被建模为monad给出一些指示吗?

好吧,因为Haskell很纯正。您需要一个数学概念来区分类型级别上的计算计算,并分别对程序流进行建模。

这意味着您将不得不使用某种IO a模型来模拟不纯的计算。然后,您需要了解这些计算组合在一起的方法,这些方法最明显,最基本,它们依次应用于>>=)和提升值return)。

通过这两个,您已经定义了一个monad(甚至都没有想到);)

此外,单子提供非常笼统,抽象厉害,这么多种类的控制流可以方便地推广在一元函数一样sequenceliftM或特殊的语法,使得unpureness没有这样一个特例。

有关更多信息,请参见函数式编程唯一性键入(我知道的唯一选择)中的monad


6

正如您所说的,这Monad是一个非常简单的结构。答案的一半是:Monad是我们可能赋予副作用功能并能够使用它们的最简单的结构。通过Monad这样做,我们可以做两件事:我们可以将纯值视为副作用值(return),并且可以将副作用函数应用于副作用值以获得新的副作用值(>>=)。失去执行上述任何一项功能的能力将使人瘫痪,因此我们的副作用类型必须至少为“” Monad,事实证明Monad足以实现到目前为止所需的一切。

另一半是:我们可以为“可能的副作用”赋予的最详细的结构是什么?我们当然可以将所有可能的副作用的空间视为一个集合(唯一需要的操作是成员资格)。我们可以将两个副作用依次进行组合,这将产生不同的副作用(或者可能是相同的副作用-如果第一个是“关闭计算机”,第二个是“写入文件”,那么结果这些只是“关机计算机”)。

好的,那么我们能说一下这个操作吗?它是关联的;也就是说,如果我们合并三个副作用,则按合并顺序无关紧要。如果执行(先写入文件然后读取套接字)然后关闭计算机,则与先写入文件然后(先读取套接字然后关闭)相同电脑)。但这不是可交换的:(“写入文件”然后“删除文件”)与(“删除文件”然后“写入文件”)有不同的副作用。而且我们有一个身份:特殊的副作用“无副作用”有效(“无副作用”然后“删除文件”与“删除文件”具有相同的副作用),此时任何数学家都在考虑“分组!”。但是,小组有相反的观点,通常没有办法消除副作用。“删除文件” 是不可逆的。因此,我们剩下的结构是一个monoid的结构,这意味着我们的副作用函数应该是monad。

有没有更复杂的结构?当然!我们可以将可能的副作用分为基于文件系统的影响,基于网络的影响等,并且我们可以提出更详尽的组成规则来保留这些细节。但这又归结为:Monad非常简单,但功能强大到足以表达我们关心的大多数属性。(特别是,关联性和其他公理使我们可以在小块中测试我们的应用程序,并确信组合应用程序的副作用将与这些块的副作用组合相同)。


4

实际上,从功能上考虑I / O的方式非常干净。

在大多数编程语言中,您都进行输入/输出操作。在Haskell中,假设编写代码不是执行操作,而是生成您想执行的操作的列表。

Monad只是用于此目的的漂亮语法。

如果您想知道为什么monad而不是其他东西,我想答案是它们是代表人们在制作Haskell时可能想到的I / O的最佳功能方式。


3

AFAIK,原因是能够在类型系统中包括副作用检查。如果您想了解更多信息,请收听那些SE-Radio剧集:第108集:Simon Peyton Jones,函数编程和Haskell第72集:Erik Meijer,LINQ


2

上面有很好的具有理论背景的详细答案。但是我想对IO monad发表看法。我不是经验丰富的haskell程序员,所以可能是天真的,甚至是错误的。但是我在某种程度上帮助我处理了IO monad(请注意,它与其他monad没有关系)。

首先,我想说的是,关于“真实世界”的示例对我来说还不太清楚,因为我们无法访问其(真实世界)的先前状态。也许它根本不涉及monad计算,但从参照透明性的角度来看是合乎需要的,通常在haskell代码中存在。

因此,我们希望我们的语言(haskell)是纯净的。但是我们需要输入/输出操作,因为没有它们,我们的程序将无法使用。而且,这些操作本质上不可能是纯粹的。因此,解决此问题的唯一方法是必须将不纯运算与其余代码分开。

monad来了。实际上,我不确定是否不存在其他具有类似所需属性的构造,但是重点是monad具有这些属性,因此可以使用它(并且可以成功使用)。主要属性是我们无法摆脱它。Monad接口没有可摆脱我们价值附近的monad的操作。其他(非IO)monad提供此类操作并允许模式匹配(例如Maybe),但这些操作不在monad接口中。另一个必需的属性是链接操作的能力。

如果我们考虑类型系统方面的需求,就会发现我们需要带构造函数的类型,该构造函数可以包装在任何变量上。构造函数必须是私有的,因为我们禁止对其进行转义(即,模式匹配)。但是我们需要函数将值放入此构造函数中(这里想到了返回)。我们需要连锁经营的方式。如果我们考虑一段时间,就会发现事实是,链接操作必须具有>> =的类型。因此,我们得出与monad非常相似的东西。我认为,如果现在使用这种结构分析可能存在的矛盾情况,我们将得出monad公理。

注意,已开发的构造与杂质没有任何共同之处。它仅具有一些属性,而我们希望这些属性必须能够处理不纯净的操作,即无转义,链接和进入方式。

现在,此选定的monad IO中的语言预定义了一些不纯运算。我们可以将这些操作结合起来以创建新的不纯操作。所有这些操作都必须具有其类型的IO。但是请注意,某些功能类型中IO的存在并不会使该功能不纯。但是据我了解,用IO类型编写纯函数是个坏主意,因为最初将纯函数和不纯函数分开是我们的想法。

最后,我想说的是,monad不会将不纯的操作变成纯操作。它仅允许有效地将它们分开。(我重复,这只是我的理解)


1
它们通过允许您键入检查效果来帮助您键入检查程序,并且您可以通过创建monad来定义自己的DSL,以限制函数可以执行的作用,以便编译器可以检查序列错误。
aoeu256 '19

来自aoeu256的评论是到目前为止所有解释中都缺少的“为什么”。(即:单子不是人类,而是编译器)
若奥·奥特罗
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.