仅仅因为某些东西可以改变并不意味着它不能用不可变的数据来建模。
在 OOish 风格中,假设您有以下内容:
a = some_obj.calculationA(some, arguments);
b = some_obj.calculationB(more, args);
return combine(a, b)
显然calculationA
并且calculationB
取决于some_obj
,您甚至可以手动some_obj
将其作为两个计算的输入。您只是不习惯看到这就是您正在做的事情,因为您考虑的是在对象上调用方法。
以最明显的方式翻译到 Haskell 的一半会给你类似的东西:
let a = calculationA some_obj some arguments
b = calculationB some_obj more args
in combine a b
手动将额外参数作为额外参数传递给所有函数实际上并没有那么麻烦some_obj
,因为无论如何这就是您在 OO 风格中所做的事情。
缺少的重要内容是 OO 样式calculationA
并且calculationB
可能会更改some_obj
,也可能在此上下文返回后使用。这在函数式风格中也很明显:
let (a, next_obj) = calculationA some_obj some arguments
(b, last_obj) = calculationB next_obj more args
in (combine a b, last_obj)
从理论的角度来看,我习惯于思考事物的方式,无论如何,这“真的”是 OOP 版本中正在发生的事情。给定的命令式代码可以访问的每个可变对象“实际上”是一个额外的输入和一个额外的输出,秘密地和隐式地传递。如果你认为函数式风格让你的程序过于复杂,因为到处都有几十个额外的输入和输出,问问你自己,当所有数据流仍然存在但被掩盖时,程序是否真的没有那么复杂?
但这就是更高的抽象(例如 monad,但它们不是唯一的)来救援的地方。最好不要把 monad 想成神奇地给你可变状态。相反,将它们视为封装模式,因此您不必像上面那样手动编写代码。当您使用State
monad 进行“有状态编程”时,所有这些通过函数的输入和输出的状态线程仍在继续,但它是以严格控制的方式完成的,并且正在发生这种情况的函数由 monadic 标记类型,所以你知道它正在发生。