Haskell 中的单子和纯度
Monads in Haskell and Purity
我的问题是 Haskell 中的单子是否真的保持了 Haskell 的纯度,如果是的话如何保持。我经常读到副作用是如何不纯的,但有用的程序需要副作用(例如 I/O)。在下一句中指出 Haskell 对此的解决方案是单子。然后在某种程度上解释了 monad,但没有真正解释它们如何解决副作用问题。
我看过this and this,我对答案的解释实际上是我自己阅读的答案——IO monad 的“动作”不是I/O 本身但是对象在执行时会执行 I/O。但我突然想到,可以对任何代码或任何已编译的可执行文件提出相同的论点。难道你不能说 C++ 程序只有在执行编译后的代码时才会产生副作用吗?所有的 C++ 都在 IO monad 中,所以 C++ 是纯粹的?我怀疑这是真的,但老实说,我不知道它以何种方式不是。事实上,Moggi (sp?) 最初不是使用 monad 来模拟命令式程序的指称语义吗?
一些背景:我是 Haskell 和函数式编程的粉丝,我希望在继续学习的过程中更多地了解这两者。例如,我了解引用透明度的好处。这个问题的动机是我是一名研究生,我将对编程语言 class 进行 2 个 1 小时的演示,一个特别涵盖 Haskell,另一个涵盖一般的函数式编程。我怀疑大多数 class 不熟悉函数式编程,也许看过一些 scheme。我希望能够(合理地)清楚地解释 monad 如何解决纯度问题,而无需进入范畴论和 monad 的理论基础,我没有时间讨论这些,而且我自己也不完全理解——当然不够好,无法展示。
不知道这个语境下的“纯度”是不是真的没有明确的定义?
我在这里看到两个主要区别:
1) 在haskell中,你可以做IO monad中没有的事情。为什么这样好?因为如果你有一个函数 definitelyDoesntLaunchNukes :: Int -> IO Int
你不知道生成的 IO 动作不会发射核武器,它可能就你所知。 cantLaunchNukes :: Int -> Int
绝对不会发射任何核武器(除非你在几乎所有情况下都应该避免任何丑陋的攻击)。
2) 在haskell中,这不仅仅是一个可爱的类比:IO 操作是第一个class 值。您可以将它们放在列表中,想放多久就放多久,除非它们以某种方式成为 main
操作的一部分,否则它们不会做任何事情。 C 最接近的是函数指针,使用起来要麻烦得多。在 C++(实际上是大多数现代命令式语言)中,你有闭包,技术上可以用于此目的,但很少 - 主要是因为 Haskell 是纯的,而它们不是。
为什么这种区别在这里很重要?那么,您要从哪里获得其他 IO actions/closures?可能 functions/methods 的一些描述。在不纯的语言中,它们本身可能会产生副作用,从而使在这些语言中隔离它们的尝试变得毫无意义。
很难在任何一个方向上做出决定性的争论,因为 "pure" 并不是特别明确。当然,某些东西 使 Haskell 从根本上不同于其他语言,它与管理副作用和 IO
类型¹ 密切相关,但尚不清楚 正是那个东西是什么。给定一个具体的定义来参考,我们可以检查它是否适用,但这并不容易:这样的定义往往要么不符合每个人的期望,要么过于宽泛而无用。
那么是什么让 Haskell 如此特别呢?在我看来,这是评估和执行之间的分离。
基础语言——与 λ 演算密切相关——都是关于前者的。您使用计算结果为其他表达式的表达式,1 + 1
到 2
。这里没有副作用,不是因为它们被抑制或移除,而是因为它们一开始就没有意义。它们不再是 model² 的一部分,就像回溯搜索是 Java(相对于 Prolog)模型的一部分一样。
如果我们只是坚持使用这种基本语言而不为 IO
添加任何功能,我认为将其命名为 "pure" 不会引起争议。作为 Mathematica 的替代品,它可能仍然有用。你会把你的程序写成一个表达式,然后在 REPL 中得到表达式的计算结果。无非是一个花哨的计算器,没有人会指责你在计算器中使用的表达式语言不纯³!
但是,当然,这太局限了。我们希望使用我们的语言来读取文件、提供网页、绘制图片、控制机器人以及与用户交互。因此,接下来的问题是如何在扩展我们的语言以完成我们想要的一切的同时保留我们喜欢的有关计算表达式的一切。
我们想出了答案? IO
。一种特殊类型的表达式,我们的类似计算器的语言可以评估它对应于执行一些有效的操作。至关重要的是,评估仍然像以前一样有效,即使对于 IO
中的事物也是如此。效果按照结果 IO
值指定的顺序执行,而不是基于它的评估方式。 IO
是我们用来将效果引入和管理其他纯表达式语言的工具。
我认为这足以使 Haskell 描述为 "pure" 有意义。
脚注
¹ 请注意我是怎么说 IO
而不是一般的 monad:monad 的概念对于许多与输入和输出无关的事物非常有用,并且 IO
类型必须是不仅仅是 一个有用的 monad。我觉得这两者在共同话题上联系得太紧密了。
² 这就是 unsafePerformIO
如此不安全的原因:它破坏了语言的核心抽象。这与在 C 中对特定寄存器进行 putzing 是一样的:它既会导致奇怪的行为,又会阻止您的代码可移植,因为它低于 C 的抽象级别。
³ 好吧,大多数情况下,只要我们忽略生成随机数之类的事情。
具有类型的函数,例如,a -> IO b
在给定相同输入时总是returns相同的 IO 操作;它是纯粹的,因为它不可能检查环境,并且遵守纯函数的所有通常规则。这意味着,除其他外,编译器可以将其所有常用优化规则应用于其类型中具有 IO
的函数,因为它知道它们仍然是纯函数。
现在,返回的 IO 操作可能 当 运行 时,查看环境、读取文件、修改全局状态等等,一旦你运行 一个动作。但是您不一定非得 运行 一个动作;您可以将其中的五个放入一个列表中,然后 运行 它们与您创建它们的顺序相反,或者根本不 运行 其中一些,如果您愿意的话;如果 IO 操作在您创建它们时隐式 运行 自己,则您无法执行此操作。
考虑这个愚蠢的程序:
main :: IO ()
main = do
inputs <- take 5 . lines <$> getContents
let [line1,line2,line3,line4,line5] = map print inputs
line3
line1
line2
line5
如果你 运行 这个,然后输入 5 行,你会看到它们以不同的顺序打印给你,并且有一个被省略,即使我们的 haskell 程序 运行s map print
按照收到的顺序排列。你不能用 C 的 printf
来做到这一点,因为它在被调用时立即执行它的 IO; haskell 的版本只是 returns 一个 IO 操作,您仍然可以将其作为第一个 class 值进行操作,并随心所欲。
小说模式:活跃
这是一个很大的挑战,我认为 wormhole could be forming in the neighbour's backyard, but I managed to grab part of a Haskell I/O implementation from an alternate reality:
class Kleisli k where
infixr 1 >=>
simple :: (a -> b) -> (a -> k b)
(>=>) :: (a -> k b) -> (b -> k c) -> a -> k c
instance Kleisli IO where
simple = primSimpleIO
(>=>) = primPipeIO
primitive primSimpleIO :: (a -> b) -> (a -> IO b)
primitive primPipeIO :: (a -> IO b) -> (b -> IO c) -> a -> IO c
回到我们略微残缺的现实中(抱歉!),我使用 Haskell I/O 的另一种形式来定义 我们的 形式 Haskell I/O:
instance Monad IO where
return x = simple (const x) ()
m >>= k = (const m >=> k) ()
有效!
小说模式:离线
My question is whether monads in Haskell actually maintain Haskell's purity, and if so how.
单子 接口 本身并不能保持对效果的限制 - 它只是一个接口,尽管它是一个愉快的多功能接口。正如我的小小说作品所展示的那样,还有其他可能的接口来完成这项工作——这只是它们在实践中使用起来有多方便的问题。
对于 Haskell I/O 的实施,控制效果的是所有相关实体,例如:
IO
、simple
、(>=>)
等
或:
IO
、return
、(>>=)
等
是抽象的 - 实现如何定义这些是保密的。
否则,您将能够设计出“新奇事物”like this:
what_the_heck =
do spare_world <- getWorld -- how easy was that?
launchMissiles -- let's mess everything up,
putWorld spare_world -- and bring it all back :-D
what_the_heck -- that was fun; let's do it again!
(你不高兴我们的现实不是那么柔韧吗?;-)
此观察扩展到 ST
(封装状态)和 STM
(并发)等类型及其管理人员(runST
、atomically
等)。对于列表 Maybe
和 Either
等类型,它们在 Haskell 中的正统定义意味着没有可见的效果。
所以当你看到一个 接口 - monadic,applicative 等 - 对于某些抽象类型,任何效果(如果它们存在)都包含在保持其 实施 私人的;避免以异常方式使用。
我的问题是 Haskell 中的单子是否真的保持了 Haskell 的纯度,如果是的话如何保持。我经常读到副作用是如何不纯的,但有用的程序需要副作用(例如 I/O)。在下一句中指出 Haskell 对此的解决方案是单子。然后在某种程度上解释了 monad,但没有真正解释它们如何解决副作用问题。
我看过this and this,我对答案的解释实际上是我自己阅读的答案——IO monad 的“动作”不是I/O 本身但是对象在执行时会执行 I/O。但我突然想到,可以对任何代码或任何已编译的可执行文件提出相同的论点。难道你不能说 C++ 程序只有在执行编译后的代码时才会产生副作用吗?所有的 C++ 都在 IO monad 中,所以 C++ 是纯粹的?我怀疑这是真的,但老实说,我不知道它以何种方式不是。事实上,Moggi (sp?) 最初不是使用 monad 来模拟命令式程序的指称语义吗?
一些背景:我是 Haskell 和函数式编程的粉丝,我希望在继续学习的过程中更多地了解这两者。例如,我了解引用透明度的好处。这个问题的动机是我是一名研究生,我将对编程语言 class 进行 2 个 1 小时的演示,一个特别涵盖 Haskell,另一个涵盖一般的函数式编程。我怀疑大多数 class 不熟悉函数式编程,也许看过一些 scheme。我希望能够(合理地)清楚地解释 monad 如何解决纯度问题,而无需进入范畴论和 monad 的理论基础,我没有时间讨论这些,而且我自己也不完全理解——当然不够好,无法展示。
不知道这个语境下的“纯度”是不是真的没有明确的定义?
我在这里看到两个主要区别:
1) 在haskell中,你可以做IO monad中没有的事情。为什么这样好?因为如果你有一个函数 definitelyDoesntLaunchNukes :: Int -> IO Int
你不知道生成的 IO 动作不会发射核武器,它可能就你所知。 cantLaunchNukes :: Int -> Int
绝对不会发射任何核武器(除非你在几乎所有情况下都应该避免任何丑陋的攻击)。
2) 在haskell中,这不仅仅是一个可爱的类比:IO 操作是第一个class 值。您可以将它们放在列表中,想放多久就放多久,除非它们以某种方式成为 main
操作的一部分,否则它们不会做任何事情。 C 最接近的是函数指针,使用起来要麻烦得多。在 C++(实际上是大多数现代命令式语言)中,你有闭包,技术上可以用于此目的,但很少 - 主要是因为 Haskell 是纯的,而它们不是。
为什么这种区别在这里很重要?那么,您要从哪里获得其他 IO actions/closures?可能 functions/methods 的一些描述。在不纯的语言中,它们本身可能会产生副作用,从而使在这些语言中隔离它们的尝试变得毫无意义。
很难在任何一个方向上做出决定性的争论,因为 "pure" 并不是特别明确。当然,某些东西 使 Haskell 从根本上不同于其他语言,它与管理副作用和 IO
类型¹ 密切相关,但尚不清楚 正是那个东西是什么。给定一个具体的定义来参考,我们可以检查它是否适用,但这并不容易:这样的定义往往要么不符合每个人的期望,要么过于宽泛而无用。
那么是什么让 Haskell 如此特别呢?在我看来,这是评估和执行之间的分离。
基础语言——与 λ 演算密切相关——都是关于前者的。您使用计算结果为其他表达式的表达式,1 + 1
到 2
。这里没有副作用,不是因为它们被抑制或移除,而是因为它们一开始就没有意义。它们不再是 model² 的一部分,就像回溯搜索是 Java(相对于 Prolog)模型的一部分一样。
如果我们只是坚持使用这种基本语言而不为 IO
添加任何功能,我认为将其命名为 "pure" 不会引起争议。作为 Mathematica 的替代品,它可能仍然有用。你会把你的程序写成一个表达式,然后在 REPL 中得到表达式的计算结果。无非是一个花哨的计算器,没有人会指责你在计算器中使用的表达式语言不纯³!
但是,当然,这太局限了。我们希望使用我们的语言来读取文件、提供网页、绘制图片、控制机器人以及与用户交互。因此,接下来的问题是如何在扩展我们的语言以完成我们想要的一切的同时保留我们喜欢的有关计算表达式的一切。
我们想出了答案? IO
。一种特殊类型的表达式,我们的类似计算器的语言可以评估它对应于执行一些有效的操作。至关重要的是,评估仍然像以前一样有效,即使对于 IO
中的事物也是如此。效果按照结果 IO
值指定的顺序执行,而不是基于它的评估方式。 IO
是我们用来将效果引入和管理其他纯表达式语言的工具。
我认为这足以使 Haskell 描述为 "pure" 有意义。
脚注
¹ 请注意我是怎么说 IO
而不是一般的 monad:monad 的概念对于许多与输入和输出无关的事物非常有用,并且 IO
类型必须是不仅仅是 一个有用的 monad。我觉得这两者在共同话题上联系得太紧密了。
² 这就是 unsafePerformIO
如此不安全的原因:它破坏了语言的核心抽象。这与在 C 中对特定寄存器进行 putzing 是一样的:它既会导致奇怪的行为,又会阻止您的代码可移植,因为它低于 C 的抽象级别。
³ 好吧,大多数情况下,只要我们忽略生成随机数之类的事情。
具有类型的函数,例如,a -> IO b
在给定相同输入时总是returns相同的 IO 操作;它是纯粹的,因为它不可能检查环境,并且遵守纯函数的所有通常规则。这意味着,除其他外,编译器可以将其所有常用优化规则应用于其类型中具有 IO
的函数,因为它知道它们仍然是纯函数。
现在,返回的 IO 操作可能 当 运行 时,查看环境、读取文件、修改全局状态等等,一旦你运行 一个动作。但是您不一定非得 运行 一个动作;您可以将其中的五个放入一个列表中,然后 运行 它们与您创建它们的顺序相反,或者根本不 运行 其中一些,如果您愿意的话;如果 IO 操作在您创建它们时隐式 运行 自己,则您无法执行此操作。
考虑这个愚蠢的程序:
main :: IO ()
main = do
inputs <- take 5 . lines <$> getContents
let [line1,line2,line3,line4,line5] = map print inputs
line3
line1
line2
line5
如果你 运行 这个,然后输入 5 行,你会看到它们以不同的顺序打印给你,并且有一个被省略,即使我们的 haskell 程序 运行s map print
按照收到的顺序排列。你不能用 C 的 printf
来做到这一点,因为它在被调用时立即执行它的 IO; haskell 的版本只是 returns 一个 IO 操作,您仍然可以将其作为第一个 class 值进行操作,并随心所欲。
小说模式:活跃
这是一个很大的挑战,我认为 wormhole could be forming in the neighbour's backyard, but I managed to grab part of a Haskell I/O implementation from an alternate reality:
class Kleisli k where
infixr 1 >=>
simple :: (a -> b) -> (a -> k b)
(>=>) :: (a -> k b) -> (b -> k c) -> a -> k c
instance Kleisli IO where
simple = primSimpleIO
(>=>) = primPipeIO
primitive primSimpleIO :: (a -> b) -> (a -> IO b)
primitive primPipeIO :: (a -> IO b) -> (b -> IO c) -> a -> IO c
回到我们略微残缺的现实中(抱歉!),我使用 Haskell I/O 的另一种形式来定义 我们的 形式 Haskell I/O:
instance Monad IO where
return x = simple (const x) ()
m >>= k = (const m >=> k) ()
有效!
小说模式:离线
My question is whether monads in Haskell actually maintain Haskell's purity, and if so how.
单子 接口 本身并不能保持对效果的限制 - 它只是一个接口,尽管它是一个愉快的多功能接口。正如我的小小说作品所展示的那样,还有其他可能的接口来完成这项工作——这只是它们在实践中使用起来有多方便的问题。
对于 Haskell I/O 的实施,控制效果的是所有相关实体,例如:
IO
、simple
、(>=>)
等
或:
IO
、return
、(>>=)
等
是抽象的 - 实现如何定义这些是保密的。
否则,您将能够设计出“新奇事物”like this:
what_the_heck =
do spare_world <- getWorld -- how easy was that?
launchMissiles -- let's mess everything up,
putWorld spare_world -- and bring it all back :-D
what_the_heck -- that was fun; let's do it again!
(你不高兴我们的现实不是那么柔韧吗?;-)
此观察扩展到 ST
(封装状态)和 STM
(并发)等类型及其管理人员(runST
、atomically
等)。对于列表 Maybe
和 Either
等类型,它们在 Haskell 中的正统定义意味着没有可见的效果。
所以当你看到一个 接口 - monadic,applicative 等 - 对于某些抽象类型,任何效果(如果它们存在)都包含在保持其 实施 私人的;避免以异常方式使用。