谁能给出一些关于为什么 Haskell 中的不纯计算被建模为 monad 的指示?
我的意思是 monad 只是一个具有 4 个操作的接口,那么在其中建模副作用的原因是什么?
return
和 (>>=)
。 x >> y
与 x >>= \\_ -> y
相同(即忽略第一个参数的结果)。我们不谈论fail
。
fail
属于 Monad
类,因为发生了历史事故;它确实属于 MonadPlus
。请注意,它的默认定义是不安全的。
假设一个函数有副作用。如果我们把它产生的所有效果都作为输入和输出参数,那么这个函数对外界来说是纯粹的。
因此,对于不纯函数
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)
)
我们一直在使用 monad
现在让我们看看我们做了什么:
我们定义了一个运算符 (~~~) :: IO b -> (b -> IO c) -> IO c 将两个不纯函数链接在一起 我们定义了一个函数 impurify :: a -> IO a 将纯值转换为不纯的。
现在我们进行识别(>>=) = (~~~)
和return = impurify
,看看?我们有一个单子。
技术说明
为了确保它真的是一个 monad,还有一些公理也需要检查:
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 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 f >>= (\x -> gx >>= h) = (f >>= g) >>= h留作练习。
谁能给出一些关于为什么 Haskell 中的不纯计算被建模为 monad 的指示?
这个问题包含一个普遍的误解。杂质和 Monad 是独立的概念。杂质不是由 Monad 建模的。相反,有一些数据类型,例如 IO
,代表命令式计算。对于其中一些类型,它们的一小部分接口对应于称为“Monad”的接口模式。此外,没有已知的对 IO
的纯/功能/外延解释(考虑到 IO
的 "sin bin" 目的,不太可能有),尽管有一个关于 World -> (a, World)
是IO a
的含义。这个故事不能如实描述 IO
,因为 IO
支持并发和非确定性。对于允许与世界进行中间计算交互的确定性计算,这个故事甚至不起作用。
有关详细说明,请参阅 this answer。
编辑:在重新阅读问题时,我认为我的答案不太正确。正如问题所说,命令式计算的模型通常是单子。提问者可能不会真正假设 monadness 以任何方式支持命令式计算的建模。
RealWorld
是“非常神奇的”。它是一个代表运行时系统正在做什么的标记,它实际上并不意味着关于现实世界的任何事情。如果不做额外的诡计,你甚至不能想出一个新的来制作“线程”;天真的方法只会创建一个单一的、阻塞的动作,并且对于何时运行有很大的不确定性。
Monad
通常确实如此。
Monad
”是指大致的forall m. Monad m => ...
,即处理任意实例。您可以对任意 monad 执行的操作几乎与您可以使用 IO
执行的操作完全相同:接收不透明的原语(分别作为函数参数或从库中),使用 return
构造空操作,或转换使用 (>>=)
以不可逆的方式赋值。在任意 monad 中编程的本质是生成一个不可撤销操作的列表:“做 X,然后做 Y,然后……”。对我来说听起来很必要!
m
视为存在可能更有帮助。此外,我的“解释”是对法律的改写; “do X”语句的列表正是通过 (>>=)
创建的未知结构上的自由幺半群;并且单子定律只是关于内函子组成的幺半群定律。
IO
是一个病态的案例,因为它几乎没有这个最小值。在特定情况下,类型可能会揭示更多的结构,从而具有实际意义;但除此之外,基于定律的单子的基本属性与 IO
一样与明确的外延对立。如果不导出构造函数、详尽地枚举原始操作或类似的东西,情况就毫无希望了。
据我了解,一个叫 Eugenio Moggi 的人首先注意到一个以前晦涩难懂的数学结构,称为“monad”,可用于对计算机语言中的副作用进行建模,从而使用 Lambda 演算来指定它们的语义。在开发 Haskell 时,有多种方法可以对不纯计算进行建模(有关更多详细信息,请参见 Simon Peyton Jones 的 "hair shirt" paper),但是当 Phil Wadler 引入 monad 时,很快就很明显这就是答案。剩下的就是历史。
谁能给出一些关于为什么 Haskell 中的不纯计算被建模为 monad 的指示?
好吧,因为 Haskell 是纯粹的。您需要一个数学概念来区分类型级别的非纯计算和纯计算,并分别对程序流进行建模。
这意味着您最终必须得到某种类型 IO a
来模拟非纯计算。然后您需要了解组合这些计算的方法,这些计算依次应用 (>>=
) 和提升值 (return
)是最明显和最基本的。
有了这两个,你已经定义了一个 monad(甚至没有想到它);)
此外,monad 提供了非常通用和强大的抽象,因此可以方便地将多种控制流泛化为 sequence
、liftM
或特殊语法等 monadic 函数,从而使不纯性不是这种特殊情况.
有关详细信息,请参阅 monads in functional programming 和 uniqueness typing(我知道的唯一替代方法)。
正如您所说,Monad
是一个非常简单的结构。一半的答案是:Monad
是我们可能赋予副作用函数并能够使用它们的最简单的结构。使用 Monad
我们可以做两件事:我们可以将纯值视为副作用值(return
),我们可以将副作用函数应用于副作用值以获得新的副作用值 (>>=
)。失去执行上述任何一项的能力都会造成严重后果,因此我们的副作用类型需要“至少”为 Monad
,事实证明 Monad
足以实现我们迄今为止所需的一切。
另一半是:对于“可能的副作用”,我们可以给出的最详细的结构是什么?我们当然可以将所有可能的副作用的空间视为一个集合(唯一需要的操作是成员资格)。我们可以通过一个接一个地执行它们来组合两种副作用,这会产生不同的副作用(或者可能是相同的副作用 - 如果第一个是“关闭计算机”,第二个是“写入文件”,那么结果组成这些只是“关闭计算机”)。
好的,那么我们能对这个操作说些什么呢?它是关联的;也就是说,如果我们结合三个副作用,那么我们按照哪个顺序进行结合并不重要。如果我们这样做(写入文件然后读取套接字)然后关闭计算机,这与执行写入文件然后(读取套接字然后关闭)相同计算机)。但它不是可交换的:("write file" then "delete file") 与 ("delete file" then "write file") 的副作用不同。我们有一个身份:特殊副作用“无副作用”有效(“无副作用”然后“删除文件”与“删除文件”的副作用相同)此时任何数学家都在思考“组!”但是群体有反面,一般来说没有办法逆转副作用。 “删除文件”是不可逆的。所以我们留下的结构是一个幺半群,这意味着我们的副作用函数应该是单子。
有没有更复杂的结构?当然!我们可以将可能的副作用分为基于文件系统的效果、基于网络的效果等等,并且我们可以提出更精细的组合规则来保留这些细节。但再次归结为:Monad
非常简单,但功能强大,足以表达我们关心的大多数属性。 (特别是,关联性和其他公理让我们可以在小块中测试我们的应用程序,并确信组合应用程序的副作用将与这些块的副作用组合相同)。
它实际上是一种以功能方式考虑 I/O 的非常简洁的方式。
在大多数编程语言中,您执行输入/输出操作。在 Haskell 中,想象编写代码不是为了执行操作,而是生成您想要执行的操作的列表。
Monads 正是这样的语法。
如果您想知道为什么 monad 与其他东西相反,我想答案是它们是人们在制作 Haskell 时可以想到的表示 I/O 的最佳功能方式。
AFAIK,原因是能够在类型系统中包含副作用检查。如果您想了解更多信息,请收听 SE-Radio 集:第 108 集:Simon Peyton Jones 谈函数式编程和 Haskell 第 72 集:Erik Meijer 谈 LINQ
上面有非常好的详细答案,具有理论背景。但我想谈谈我对 IO monad 的看法。我不是经验丰富的 haskell 程序员,所以可能是很幼稚甚至是错误的。但我在一定程度上帮助我处理了 IO monad(注意,它与其他 monad 无关)。
首先我想说,“真实世界”的例子对我来说不太清楚,因为我们无法访问它(真实世界)以前的状态。可能它根本与 monad 计算无关,但在引用透明的意义上它是需要的,这通常出现在 haskell 代码中。
所以我们希望我们的语言(haskell)是纯粹的。但是我们需要输入/输出操作,因为没有它们我们的程序就没有用处。这些操作就其性质而言不可能是纯粹的。所以解决这个问题的唯一方法是我们必须将不纯的操作与其余代码分开。
monad 来了。实际上,我不确定是否不存在具有类似所需属性的其他构造,但关键是 monad 具有这些属性,因此可以使用(并且成功使用)。主要属性是我们无法摆脱它。 Monad 接口没有操作来摆脱我们周围的 monad 值。其他(非 IO)monad 提供此类操作并允许模式匹配(例如 Maybe),但这些操作不在 monad 接口中。另一个必需的属性是链接操作的能力。
如果我们从类型系统的角度考虑我们需要什么,我们会发现我们需要带有构造函数的类型,它可以包裹在任何变量上。构造函数必须是私有的,因为我们禁止逃避它(即模式匹配)。但是我们需要函数将值放入这个构造函数(这里想到了 return)。我们需要链式操作的方式。如果我们考虑一段时间,我们会发现,链接操作必须具有 >>= 的类型。所以,我们得到了与 monad 非常相似的东西。我想,如果我们现在用这个结构分析可能的矛盾情况,我们就会来到单子公理。
请注意,开发的构造与杂质没有任何共同之处。它只有属性,我们希望能够处理不纯的操作,即不转义、链接和进入的方式。
现在,这个选定的 monad IO 中的语言预定义了一些不纯的操作。我们可以结合这些操作来创建新的非纯操作。所有这些操作都必须在其类型中包含 IO。但是请注意,某些函数类型中的 IO 的存在不会使该函数不纯。但据我了解,编写带有 IO 类型的纯函数是个坏主意,因为我们最初的想法是分离纯函数和不纯函数。
最后,我想说的是,monad 不会将不纯的操作变成纯操作。它只允许有效地将它们分开。 (我再说一遍,这只是我的理解)
RealWorld
概括为...好吧,您会看到。IO
,因为后者支持交互、并发和非确定性。有关更多指示,请参阅我对这个问题的回答。IO
,但RealWorld
实际上并不代表现实世界,它只是保持操作有序的标记(“神奇”在于RealWorld
是 GHC Haskell 的唯一唯一性类型)IO
。对于其他类型,真相与通常的 Haskell 语义一起存在于源代码中。a
,而应该应用于worldX
。我会再检查一下,然后再编辑。接得好! :) 在 SO-as-it-should-have-been 中,您将因此修复获得巨额声望奖励!