有没有人可以指出为什么Haskell中的不纯计算被建模为monad?
我的意思是monad只是一个有4个操作的界面,那么建模副作用的原因是什么呢?
答案 0 :(得分:283)
假设某个功能有副作用。如果我们将它产生的所有效果作为输入和输出参数,那么该函数对于外部世界是纯粹的。
所以对于一个不纯的函数
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
再次变得纯洁。我们定义了一个参数化数据类型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
没有触及现实世界。
现在假设我们也希望将文件内容设为大写。 Uppercasing是一个纯函数
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)
)
现在让我们看看我们做了什么:
(~~~) :: IO b -> (b -> IO c) -> IO c
impurify :: a -> IO a
,它将纯值转换为不纯。现在我们进行身份识别(>>=) = (~~~)
和return = impurify
了,看看了吗?我们有一个单子。
(要检查它是否真的是一个单子,应该满足几个公理:
(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) a worldX = let (resF, worldY) = impuify a worldX
in f resF worldY
= let (resF, worldY) = (a, worldX)
in f resF worldY
= f a worldX
(3)f >>= (\x -> g x >>= h) = (f >>= g) >>= h
运动。)
答案 1 :(得分:43)
有没有人可以指出为什么Haskell中的无法计算被建模为monad?
这个问题包含了广泛的误解。
杂质和Monad是独立的概念。
杂质是不由Monad建模。
相反,有一些数据类型,例如IO
,代表命令式计算。
对于其中一些类型,其界面的一小部分对应于称为“Monad”的界面模式。
此外,对于IO
没有已知的纯/功能/外延解释(并且考虑到IO
的{{3}}目的,不可能有一个),尽管有一个常见的故事约World -> (a, World)
是IO a
的意思。
这个故事无法真实地描述IO
,因为IO
支持并发性和非确定性。
当确定性计算允许中间计算与世界互动时,这个故事甚至都不起作用。
有关详细说明,请参阅"sin bin"。
编辑:在重新阅读问题时,我认为我的答案没有按计划进行。 正如问题所说,命令式计算的模型经常变成monad。 提问者可能并不认为monadness能够以任何方式对命令式计算进行建模。
答案 2 :(得分:13)
据我所知,有人称Eugenio Moggi首先注意到一个名为“monad”的先前模糊的数学结构可用于模拟计算机语言中的副作用,因此使用Lambda演算来指定它们的语义。当Haskell被开发出来时,有不同的方式对不纯的计算进行建模(参见Simon Peyton Jones'"hair shirt" paper了解更多细节),但是当Phil Wadler介绍monad时,很快就会发现这就是The Answer。其余的都是历史。
答案 3 :(得分:8)
有没有人可以指出为什么Haskell中的无法计算被建模为monad?
好吧,因为Haskell 纯。您需要一个数学概念来区分类型级上的 unure计算和纯以及程序流的模型分别在。
这意味着你必须得到一些类型IO a
来模拟不合理的计算。然后你需要知道组合这些按顺序应用(>>=
)和提升值({{ 1}})是最明显和最基本的。
有了这两个,你已经定义了一个monad (甚至没有想到它);)
此外,monad提供非常通用和强大的抽象,因此可以在return
,sequence
或特殊语法等一元函数中方便地推广多种控制流,使不确定不是一个特例。
有关详细信息,请参阅monads in functional programming和uniqueness typing(我知道的唯一替代方案)。
答案 4 :(得分:6)
正如您所说,Monad
是一个非常简单的结构。答案的一半是:Monad
是我们可能给副作用函数提供的最简单的结构,并且能够使用它们。使用Monad
,我们可以做两件事:我们可以将纯值视为副作用值(return
),我们可以将副作用函数应用于副作用值以获得新的副作用价值(>>=
)。失去做这些事情的能力将会瘫痪,所以我们的副作用类型需要“至少”Monad
,事实证明Monad
足以实现我们所需要的一切到目前为止。
另一半是:我们可以给“可能的副作用”最详细的结构是什么?我们当然可以将所有可能的副作用空间视为一组(唯一需要的是会员资格)。我们可以通过一个接一个地做两个副作用来组合,这会产生不同的副作用(或者可能是同一个 - 如果第一个是“关闭计算机”而第二个是“写文件”,那么结果撰写这些只是“关机计算机”)。
好的,那么我们能说些什么呢?它是联想的;也就是说,如果我们结合三个副作用,那么我们合并的顺序并不重要。如果我们这样做(写文件然后读取套接字)然后关闭计算机,那就像写文件那样(读取套接字然后关闭)电脑)。但它不是可交换的:(“写文件”然后“删除文件”)是一种不同的副作用(“删除文件”然后“写入文件”)。我们有一个身份:特殊的副作用“无副作用”有效(“无副作用”,然后“删除文件”与“删除文件”的副作用相同)此时任何数学家都在想“群组!”但群体反转,一般情况下无法反转副作用; “删除文件”是不可逆转的。所以我们留下的结构是一个幺半群,这意味着我们的副作用函数应该是monad。
是否有更复杂的结构?当然!我们可以将可能的副作用划分为基于文件系统的效果,基于网络的效果等等,我们可以提出更精细的构图规则来保留这些细节。但同样,它归结为:Monad
非常简单,但功能强大,足以表达我们关心的大部分属性。 (特别是,关联性和其他公理让我们以小块的形式测试我们的应用,并确信组合应用的副作用将与碎片的副作用相同)。
答案 5 :(得分:4)
实际上,以功能的方式考虑I / O非常简洁。
在大多数编程语言中,您执行输入/输出操作。在Haskell中,想象编写代码不是为了做操作,而是要生成一个你想要做的操作列表。
Monads就是完全相同的语法。
如果你想知道为什么monad与其他东西相反,我想答案是它们是表示人们在制作Haskell时可以想到的I / O的最佳功能方式。
答案 6 :(得分:3)
AFAIK,原因是能够在类型系统中包含副作用检查。如果您想了解更多信息,请收听这些SE-Radio集: 第108集:Simon Peyton Jones关于功能编程和Haskell 第72集:关于LINQ的Erik Meijer
答案 7 :(得分:2)
以上有非常好的详细答案和理论背景。但我想对IO monad给出我的看法。我没有经验的哈斯克尔程序员,所以可能是相当幼稚甚至错误。但是我帮助我在某种程度上处理IO monad(请注意,它与其他monad无关)。
首先我要说的是,“真实世界”的例子对我来说不太清楚,因为我们无法访问其(现实世界)以前的状态。可能它根本不涉及monad计算,但在参考透明度的意义上是期望的,这通常出现在haskell代码中。
所以我们希望我们的语言(haskell)是纯粹的。但是我们需要输入/输出操作,因为没有它们我们的程序就没用了。而这些行动本质上并不纯粹。因此,处理此问题的唯一方法是将不纯操作与其余代码分开。
monad来了。实际上,我不确定,不存在具有类似所需属性的其他构造,但关键是monad具有这些属性,因此可以使用它(并且它已成功使用)。主要财产是我们无法摆脱它。 Monad接口没有操作来摆脱我们值周围的monad。其他(非IO)monad提供此类操作并允许模式匹配(例如,Maybe),但这些操作不在monad接口中。另一个必需的属性是连锁经营的能力。
如果我们在类型系统方面考虑我们需要什么,我们就会发现我们需要带有构造函数的类型,它可以包含在任何代码中。构造函数必须是私有的,因为我们禁止从中逃避(即模式匹配)。但是我们需要函数来将值放入这个构造函数中(这里回想起来)。我们需要连锁经营的方式。如果我们考虑一段时间,我们将会发现,链接操作的类型必须为&gt;&gt; = has。所以,我们来到monad非常相似的东西。我想,如果我们现在用这个结构来分析可能的矛盾情况,我们将会看到monad公理。
注意,开发的构造与杂质没有任何共同之处。它只有属性,我们希望能够处理不纯的操作,即不逃避,链接和进入的方式。
现在,一些不纯的操作由此选定的monad IO中的语言预定义。我们可以将这些操作结合起来创建新的不可操作的操作所有这些操作都必须在其类型中具有IO。但请注意,某些函数类型中IO的存在不会使此函数不纯。但据我所知,在类型中编写带有IO的纯函数是个坏主意,因为最初我们的想法就是将纯函数和不纯函数分开。
最后,我想说,monad不会将纯粹的操作变成纯粹的操作。它只允许有效地分离它们。 (我再说一遍,这只是我的理解)