使用函数式编程与命令式编程递增计数器

Incrementing a counter with functional programming vs imperative programming

我可能问了一个很愚蠢的问题,请见谅。

我是一名 Java 和 C# 后端工程师,对 OOP 设计模式有相对较好的了解。我最近发现了关于 OOP 与函数式编程的争论,我根本无法解决的问题是:如果没有状态,那么我们如何根据用户输入更新元素?

这是一个小例子来说明我面临的问题(我知道 JS 不是一种严格的函数式语言,但我认为它相对较好地说明了我的问题。):

假设我们有一个小网页,它只显示一个计数器并在用户每次单击按钮时递增其值:

<body>
    The count is <span id="data">0</span><br>
    <button onClick="inc()">Increment</button>
</body>

现在有严格的命令式方法,使用计数器变量来存储计数器的状态:

let data;
window.onload = function(){
    data = document.getElementById("data");
}
let counter = 0;
function inc(){
    data.innerHTML = ++counter;
}

更实用的方法(至少在我的理解中)是以下代码:

let data;
window.onload = function(){
    data = document.getElementById("data");
}
function writeValue(val){
    data.innerHTML = val;
}
function doIncrement(val){
    return val + 1;
}
function readValue(){
    return parseInt(data.innerHTML);
}
function inc(){
    writeValue(doIncrement(readValue()));
}

我现在面临的问题是,虽然数据变量永远不会改变,但数据的状态仍然会随着时间而改变(每次更新计数器时)。我真的没有看到任何真正的解决方案。当然,需要在某处跟踪计数器的状态,以便它递增。我们也可以在每次需要读取或写入数据时调用 document.getElementById("data"),但问题本质上是一样的。我以某种方式跟踪页面的状态以便稍后处理它。

编辑:请注意,我已将值 val 重新分配(有人告诉我,这在 FP 中是一件坏事)到 writeValue(val) 函数中的变量 innerHTML。这是我开始质疑我的方法的确切路线。

TL;DR:您将如何处理自然会以功能方式发生变化的数据?

这个问题似乎源于对函数式编程(FP)没有状态的误解。虽然这个概念是可以理解的,但事实并非如此。

简而言之,FP 是一种明确区分 pure functions and everything else (often called impure actions). Simon Peyton-Jones (SPJ, one of the core Haskell 开发人员的编程方法)曾经在一次演讲中说过,如果你不能有任何副作用,那么纯函数唯一能做的就是加热 CPU,此后一位学生评论说这也是一个副作用。 (很难找到这个故事的确切来源。我记得曾看过 SPJ 的采访,他在其中讲述了这个故事,但在 2022 年的网络上搜索视频中的引述仍然很困难。)

  • 更改屏幕上的像素是一种副作用。
  • 发送电子邮件是一种副作用。
  • 删除文件是一种副作用。
  • 在数据库中创建行是一种副作用。
  • 改变一个内部变量,通过级联后果,导致上述任何事情发生,是一种副作用。

不可能编写出没有副作用的(有用的)软件。

此外,纯函数也不允许 non-deterministic 行为。这排除了更多必要的操作:

  • 得到一个(真正的)随机数是non-deterministic。
  • 获取时间或日期是non-deterministic。
  • 读取文件是non-deterministic。
  • 查询数据库是non-deterministic。
  • 等等

FP 承认所有这些不纯的行为都需要发生。理念上的不同在于对纯函数的强调。纯函数具有许多理想的特性(可预测性、引用透明性、可能的记忆化、可测试性),因此值得追求有利于此类函数的编程哲学。

A functional architecture is one that minimises the impure actions to their essentials. One label for that is Functional Core, Imperative Shell,您将所有不纯的操作推到系统的边缘。例如,这将包括一个 HTML 计数器。实际上更改 HTML 元素仍然是必要的,而生成新值所需的计算可以作为纯函数实现。

不同的语言对于它们如何明确地模拟纯函数和不纯操作之间的区别有不同的方法。大多数语言(甚至 'functional' 语言,如 F# and Clojure)都没有明确区分,因此程序员需要牢记这种区分。

Haskell 是一种以做出这种区分而闻名的语言。它使用 IO monad 显式地模拟不纯的行为。我试图在一篇文章中使用 C# 作为示例语言为 object-oriented 程序员解释 IOThe IO Container.