由于副作用破坏了引用透明度,它们不违背函数式语言的观点吗?
4 回答
纯函数式编程语言使用两种技术来模拟副作用:
1) 表示外部状态的世界类型,其中该类型的每个值由类型系统保证仅使用一次。
在使用这种方法的语言中,函数可能分别具有 和print
类型。read
(string, world) -> world
world -> (string, world)
它们可能会这样使用:
let main w =
let w1 = print ("What's your name?", w) in
let (name, w2) = read w1 in
let w3 = print ("Your name is " ^ name, w2) in
w3
但不是这样:
let main w =
let w1 = print ("What's your name?", w) in
let (name, w2) = read w in
let w3 = print ("Your name is " ^ name, w2) in
w3
(因为 w 被使用了两次)
所有具有副作用的内置函数都将获取并返回一个世界值。由于所有具有副作用的函数要么是内置函数,要么是调用具有副作用的其他函数,这意味着所有具有副作用的函数都需要获取和返回一个世界。
这样就不可能使用相同的参数两次调用具有副作用的函数,并且不会违反引用透明性。
2) 一个 IO monad,所有具有副作用的操作都必须在该 monad 内执行。
使用这种方法,所有具有副作用的操作都将具有 type io something
。例如print
,将是一个具有 type 的函数,string -> io unit
并且read
将具有 type io string
。
访问执行操作的值的唯一方法是使用“monadic bind”操作(例如在 haskell 中称为 >>=),将 IO 操作作为一个参数,将一个描述如何处理结果的函数作为另一个参数操作数。
上面的示例使用 monadic IO 如下所示:
let main =
(print "What's your name?") >>=
(lambda () -> read >>=
(lambda name -> print ("Your name is " ^ name)))
有几个选项可用于以函数式语言处理 I/O。
- 不要纯洁。许多函数式语言并不是纯粹的函数式。他们更多的是支持 函数式编程而不是强制执行它。这是迄今为止函数式编程中 I/O 问题最常见的解决方案。(例如:Lisp、Scheme、Standard ML、Erlang 等)
- 流转换。早期的 Haskell I/O 就是这样完成的。如果您想了解更多信息,请查看下面的链接以获取详细信息。(提示:你可能不知道。)
- 继续传递 I/O(其他答案中提到的“世界传递”)。在这个中,您通过 I/O 传递一个数据令牌,它充当必要的“不同值”以保持引用完整性。如果没有记错的话,这会被几种 ML 方言使用。
- 上面的“延续”或“世界”事物可以包装在各种数据类型中,最著名的(最著名的)是在 Haskell 中使用 monads。请注意,这在理论上是相同的事情,但删除了跟踪“世界”/“延续”状态变量的单调乏味。
有一篇研究论文详尽地分析了这些。
函数式 I/O 是一个正在进行的研究领域,还有其他语言以有趣和令人费解的方式解决这个问题。 霍尔逻辑被用于一些研究语言。其他人(如Mercury)使用uniqueness typing。还有一些(如 Clean)使用效果系统。其中我对水星的接触非常非常有限,所以我不能对细节发表评论。但是,如果您对这个方向感兴趣,有一篇论文详细介绍了 Clean 的 I/O 系统。
据我所知,如果你想在函数式语言中产生副作用,你必须明确地对它们进行编码。
由于副作用破坏了引用透明度,它们不违背函数式语言的观点吗?
这取决于功能语言:
标准 ML允许像大多数过程语言(例如 Fortran、Algol、Pascal、C 等)一样自由使用副作用。
Haskell通过使用和等抽象数据类型来限制副作用
IO
,这有助于保持引用透明性。ST
STM
Clean还限制了副作用,但它通过扩展类型系统做到了这一点。
函数式语言如何模拟副作用?
一种不经常提及的方法依赖于伪数据:在可访问的结构化值(通常是树)中传达的单个一次性抽象值,只有在最初使用每个抽象值时才会出现副作用。有关更多信息,请参阅 F. Warren Burton 的Nondeterminism with Referential Transparency in Functional Programming Language。在 GHC 中也可以找到一个工作示例:它的Unique
name-suppy 类型。