我现在处理函数式编程的概念有一段时间了 [...] 但有一件事我不明白:如何在将自己限制为纯函数时处理副作用。
Claus Reinke 在撰写论文时提出了类似的问题- 从 210 页的第 10 页开始:
程序与外部环境(例如,由输入/输出设备、文件系统等组成)之间的交互必须如何用一种从外部世界的存在中抽象出来的编程语言来描述?
对于像 Haskell 这样追求函数数学概念的函数式语言,这带来了另一个问题:
这似乎违反直觉...
在“传统编程语言”几乎总是势在必行的时代:
在 1960 年代,一些研究人员开始致力于证明有关程序的事情。努力证明:
虽然这些是抽象的目标,但它们实际上都与“调试程序”的实际目标相同。
这项工作出现了几个难题。一个是规范的问题:在证明一个程序是正确的之前,必须正式且明确地指明“正确”的含义。开发了用于指定程序含义的正式系统,它们看起来很像编程语言。
编程语言剖析(第 353 页,共 600 页),Alice E. Fischer 和 Frances S. Grodzinsky。
(强调补充。)
克劳斯·雷因克(Claus Reinke)做出了类似的观察——来自210 页的第 65 页:
以单子风格编写的交互式程序的符号非常接近命令式语言中使用的符号。
但仍有成功的可能:
研究人员开始分析为什么证明用传统语言编写的程序往往比证明数学定理更难。传统语言的两个方面成为麻烦的根源,因为它们很难在数学系统中建模:可变性和顺序性。
编程语言剖析(同页。)
(“非常困难”,但不是“不可能”——而且显然不太实用。)
也许剩下的问题将在2260年代到 2060 年代的某个时间得到解决,使用一组扩展的基本数学概念。在那之前,我们只需要do
使用笨拙的以 I/O 为中心的类型,例如:
IO
不是外延的 。
康纳尔埃利奥特。
既然IO
已经(有点)在别处解释过,让我们尝试一些不同的东西——受 Haskell 的FFI启发:
data FF a b -- abstract "tag" type
foreign import ccall "primArrFF" arrFF :: (a -> b) -> FF a b
foreign import ccall "primPipeFF" pipeFF :: FF a b -> FF b c -> FF a c
foreign import ccall "primBothFF" bothFF :: FF a b -> FF c d -> FF (a, c) (b, d)
foreign import ccall "primAltFF" altFF :: FF a b -> FF c d -> FF (Either a c) (Either b d)
foreign import ccall "primAppFF" appFF :: FF (FF a b, a) b
foreign import ccall "primTieFF" tieFF :: FF (a, c) (b, c) -> FF a b
⋮
foreign import ccall "primGetCharFF" getChar :: FF () Char
foreign import ccall "primPutCharFF" putChar :: FF Char ()
⋮
至于Main.main
:
module Main(main) where
main :: FF () ()
⋮
...可以扩展为:
module Main() where
foreign export ccall "FF_main" main :: FF () ()
⋮
( , et alFF
的例子留作练习 ;-)Arrow
因此,就目前而言(2022 年 1 月),当您限制自己使用普通的 Haskell 函数时,如何处理副作用:
- 为具有可观察效果的实体引入适当的抽象类型(
FF a b
);
- 引入两组原语 - 一组组合子(
arrFF
, pipeFF
, bothFF
, altFF
, appFF
,tieFF
等)和一组非标准态射(getChar
,putChar
等);
- 然后,您可以
Main.main :: FF () ()
使用普通的 Haskell 函数和那些FF
原语进行定义。
通过这种方式,普通的 Haskell 函数可以保持没有副作用——它们实际上并不“运行”FF
实体,而是从其他(通常更小的)实体构建它们。唯一FF
要“运行”的实体是Main.main
通过其外部导出,由运行时系统调用(通常以允许副作用的命令式语言实现)。