为什么在Haskell中将副作用build模为monad?
任何人都可以提供一些关于为什么Haskell中的不纯计算被build模为monad的指针吗?
我的意思是monad只是一个有4个操作的接口,那么在其中build模的副作用是什么呢?
假设一个函数有副作用。 如果我们把所有产生的效果作为input和输出参数,那么这个函数对于外界来说是纯粹的。
所以对于不纯的function
f' :: Int -> Int
我们添加RealWorld的考虑
f :: Int -> RealWorld -> (Int, RealWorld) -- input some states of the whole world, -- modify the whole world because of the a side effects, -- then return the new world.
那么f
是纯粹的。 我们定义一个参数化的数据typesIO a = RealWorld -> (a, RealWorld)
,所以我们不需要多次inputRealWorld
f :: Int -> IO Int
对于程序员来说,直接处理RealWorld是非常危险的,特别是如果程序员得到了RealWorldtypes的值,他们可能会尝试复制它,这是基本上不可能的。 (例如,试图复制整个文件系统,你将把它放在哪里?)因此,我们对IO的定义也封装了整个世界的状态。
如果我们不能把它们连在一起,这些不纯的function就没用了。 考虑
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
没有触及现实世界。
现在假设我们也想使文件内容为大写。 大写是一个纯粹的function
upperCase :: String -> String
但要使其进入现实世界,必须返回一个IO String
。 很容易解除这样的function:
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
impureUpperCase = impurify . upperCase
,我们可以写
printUpperCaseFile = getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn
(注意:通常我们写getLine ~~~ getContents ~~~ (putStrLn . upperCase)
)
现在让我们看看我们做了什么:
- 我们定义了一个运算符
(~~~) :: IO b -> (b -> IO c) -> IO c
,它将两个不纯的函数链接在一起 - 我们定义了一个函数
impurify :: a -> IO a
,它将纯粹的值转换为不纯。
现在我们进行识别(>>=) = (~~~)
并return = impurify
,看看? 我们有一个monad。
(要检查它是否真的是单子,应该满足几个公理:
(1) return a >>= f = fa
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 = fa worldX
(2) f >>= return = f
(f ~~~ impurify) a worldX = let (resF, worldY) = impuify a worldX in f resF worldY = let (resF, worldY) = (a, worldX) in f resF worldY = fa worldX
(3) f >>= (\x -> gx >>= h) = (f >>= g) >>= h
行使。)
任何人都可以提供一些关于为什么Haskell中的不确定计算被build模为单子的一些指针?
这个问题蕴含着广泛的误解。 杂质和Monad是独立的概念。 杂质不是由Monadbuild模的。 相反,有一些数据types,比如IO
,代表了命令式的计算。 对于其中的一些types,其接口的一小部分对应于称为“Monad”的接口模式。 此外,对IO
认识并不存在已知的纯粹的/function的/外延的解释(考虑到IO
的“罪恶”目的,不太可能有一个),尽pipe有关于World -> (a, World)
是IO a
的意思。 那个故事不能如实地描述IO
,因为IO
支持并发和非确定性。 当确定性计算允许与世界进行中等计算交互时,这个故事甚至不起作用。
有关更多解释,请参阅此答案 。
编辑 :在重读这个问题时,我不认为我的回答是相当正确的。 正如问题所说,命令式计算的模型经常变成单子。 提问者可能并不真的假定单调性以任何方式实现了命令式计算的build模。
据我所知,有人叫欧金尼奥·莫吉 ( Eugenio Moggi)首先注意到,以前一个叫做“monad”的模糊math结构可以用来模拟计算机语言中的副作用,因此用Lambda微积分来指定它们的语义。 当Haskell被开发出来的时候,有不同的方式来模拟不纯的计算(见西蒙·佩顿·琼斯(Simon Peyton Jones)的“发衬衫”文章 ),但是当Phil Wadler介绍单子时,很明显这就是答案。 剩下的就是历史。
任何人都可以提供一些关于为什么Haskell中的不确定计算被build模为单子的一些指针?
那么,因为Haskell是纯粹的 。 您需要一个math概念来区分types级别的 不确定计算和纯粹 计算,并分别对程序stream程进行build模。
这意味着你将不得不结束一些types的IO a
模型的不确定的计算。 那么你需要知道将这些计算结合起来 应用的方法 ( >>=
)和提升值 ( return
)是最明显和最基本的方法。
有了这两个,你已经定义了一个monad (甚至没有考虑到它);)
另外monad提供了非常一般和强大的抽象 ,所以多种控制stream可以方便地推广到sequence
, liftM
或特殊语法等一元函数中,使得不适合不是这种特殊情况。
有关更多信息,请参阅函数式编程和唯一性types (我知道的唯一select)中的monad 。
正如你所说, Monad
是一个非常简单的结构。 答案的一半是: Monad
是最简单的结构,我们可能会给副作用函数并能够使用它们。 对于Monad
我们可以做两件事情:我们可以把一个纯粹的价值作为一个副作用价值( return
),我们可以将一个副作用函数应用于一个副作用价值,以获得一个新的副作用价值( >>=
)。 失去做这些事情的能力将会被削弱,所以我们的副作用types需要至less是“ Monad
,事实certificate, Monad
足以实现我们迄今为止所需要的一切。
另一半是:我们可以给“可能的副作用”最详细的结构是什么? 我们当然可以把所有可能的副作用的空间想象成一个集合(唯一需要的操作是成员资格)。 我们可以通过一个接一个地把两个副作用结合起来,这会产生不同的副作用(或者可能是同一个副作用 – 如果第一个是“关机”,第二个是“写入文件”,那么结果组成这些的只是“关机电脑”)。
好的,那么我们该怎么说这个操作呢? 这是联想; 也就是说,如果我们把三个副作用结合在一起,那么我们在哪一个顺序中进行合并并不重要。如果我们这样做(写入文件,然后读取socket)然后closures计算机,就像写入文件那样(读取socket然后closures电脑)。 但是不能交换:(“写入文件”,然后“删除文件”)与(“删除文件”,然后“写入文件”)是不同的副作用。 而且我们有一个身份:“无副作用”的特殊副作用(“无副作用”,那么“删除文件”就是和“删除文件”一样的副作用)。现在任何一个math家都在想“群! 但是,群体已经倒过来了,总的来说,没有办法扭转副作用; “删除文件”是不可逆转的。 所以我们留下的结构就是一个幺半群,这意味着我们的副作用应该是monad。
有更复杂的结构吗? 当然! 我们可以将可能的副作用分为基于文件系统的效果,基于networking的效果和更多,我们可以拿出更详细的构图规则来保存这些细节。 但是也可以归结为: Monad
非常简单,但却足够强大,可以expression我们关心的大部分属性。 (特别是,关联性和其他公理使我们能够testing我们的应用程序,并确信组合应用程序的副作用将与组件的副作用相同)。
实际上,以function性的方式来思考I / O是一个相当干净的方式。
在大多数编程语言中,你可以做input/输出操作。 在Haskell中,设想编写代码不要执行这些操作,而是要生成一个您想要执行的操作的列表。
Monad就是那个漂亮的语法。
如果你想知道monads为什么不是别的,我猜的答案是,他们是代表I / O的最好的函数方式,人们在做Haskell时可以想到它。
AFAIK,原因是能够在types系统中包含副作用检查。 如果你想知道更多,听听这些SE收音机情节:第108集:Simon Peyton Jonesfunction编程和Haskell第72集:Erik Meijer on LINQ
上面有非常好的理论背景的详细答案。 但是我想给出我对IO monad的看法。 我没有经验的Haskell程序员,所以可能是相当天真的,甚至是错误的。 但是我帮助我在一定程度上处理IO monad(注意,它不涉及其他单子)。
首先我想说的是,那个“现实世界”的例子对我来说不是太清楚,因为我们不能访问它的(现实世界)以前的状态。 可能它根本不涉及monad计算,但它是希望在参考透明的意义上,通常在haskell代码中出现。
所以我们希望我们的语言(哈斯克尔)是纯粹的。 但是我们需要input/输出操作,因为没有它们,我们的程序就无法使用。 而这些行动本身并不纯粹。 因此,处理这个问题的唯一方法就是将不纯的操作与其他代码分开。
这里monad来。 实际上,我不确定不存在其他具有相似所需属性的构造,但重点是monad具有这些属性,因此可以使用(并且成功地使用它)。 主要属性是我们无法逃避。 Monad界面没有操作来摆脱我们价值的monad。 其他(不是IO)monad提供这样的操作并允许模式匹配(例如Maybe),但这些操作不在monad接口中。 另一个要求的属性是连锁经营的能力。
如果我们从types系统的angular度思考我们需要什么,那么我们就会遇到这样一个事实:我们需要带有构造函数的types,这个构造函数可以围绕任何一个值。 构造函数必须是私有的,因为我们禁止从它逃脱(即模式匹配)。 但是我们需要函数来为这个构造函数赋值(在这里可以回想起来)。 我们需要连锁经营的方式。 如果我们思考一段时间,我们会发现,链接操作必须具有types>> =。 所以,我们来了一个非常类似monad的东西。 我想,如果我们现在用这个结构分析可能的矛盾情况,我们将会来到monad公理。
请注意,开发的构造与杂质没有任何共同之处。 它只有属性,我们希望能够处理不纯的操作,即不逃避,链接和进入的方式。
现在一些不纯的操作是由这个选定的monad IO中的语言预定义的。 我们可以结合这些操作来创build新的不可行操作。 所有这些操作都必须具有IOtypes。 但是请注意,某些函数的typesIO的存在不会使这个函数不纯。 但据我所知,用typesIO编写纯函数是个坏主意,因为最初我们的想法是分离纯粹和不纯的函数。
最后,我想说,monad不会把不纯的操作变成纯粹的操作。 它只允许有效地分开它们。 (我重复一遍,这只是我的理解)