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



我可能在这里问了一个非常愚蠢的问题,请原谅我。

我是一名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重新分配给了writeValue(val)函数中的变量innerHTML(有人告诉我,这在FP中是件坏事(。这正是我开始质疑我的方法的起点。

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

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

简而言之,FP是一种编程方法,它明确区分纯函数和其他一切(通常称为不纯操作(。Simon Peyton Jones(SPJ,Haskell的核心开发人员之一(曾经做过一次讲座,他说了一些大意是,如果你没有任何副作用,那么你对纯函数唯一能做的就是加热CPU,之后一名学生评论说,这也是一个副作用。(很难找到这个故事的确切来源。我记得我看过SPJ的一次采访,他在采访中讲述了这个故事,但在2022年,在网上搜索视频中的引用仍然很困难。(

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

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

此外,纯函数也不允许非确定性行为。这排除了更必要的行动:

  • 获取(真正的(随机数是不确定的
  • 获取时间或日期是不确定的
  • 读取文件是不确定的
  • 查询数据库是不确定的
  • 等等

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

功能性架构是一种将不纯洁的行为降至最低的架构。其中一个标签是功能核心,命令外壳,你把所有不纯洁的行为推到系统的边缘。例如,这将包括一个HTML计数器。实际上,更改HTML元素是必须的,而生成新值所需的计算可以作为纯函数来实现。

不同的语言有不同的方法来明确地建模纯函数和非纯动作之间的区别。大多数语言(甚至像F#和Clojure这样的"函数式"语言(都没有明确地做出这种区分,所以程序员应该记住这种分离。

Haskell是一种著名的语言,它确实做出了这种区分。它使用IOmonad来显式地为不纯净的行为建模。在一篇文章《IO容器》中,我试图用C#作为示例语言为面向对象的程序员解释IO

最新更新