鲍勃大叔:函数式编程真的不需要面向对象吗?


什么是类?根据字典,一个类是:

一组、集合、群体或配置,其中的成员被认为具有某些共同的属性或特征;一个种类或类别。

现在在阅读下一段时考虑一下这个定义:

在OO语言中,我们将我们的程序组织成具有类似特征的对象类别。我们用它们共同的属性和行为来描述这些对象。我们努力创造分类的层次,使这些对象能够适应其中。我们认为更高层次的分类是抽象的,允许表达独立于不相关细节的一般真理。(事实上,我曾经把抽象定义为:放大基本的东西,消除不相关的东西)。
banq:偏见是抽象的代名词

1966年,分类(classification)抽象的力量促使Simula的作者们创造了关键词“类class”。1980年,Bjarne Stroustrup延续了这一惯例,在C++中使用了“类class”这个关键词。这实际上有些奇怪,因为C语言已经有了关键词struct,其含义几乎完全相同。但是,“类”这个词的威力是很强的。

在90年代中期,这个词的力量使Java(然后是C#)的作者宣布并强制要求程序中的一切都必须是“类class”的一部分。这是一个戏剧性的过度行为。在我看来,一些被Java强行纳入“类”的东西根本就不应该出现在“类”中。例如,java.lang.Math这个类实际上只是一批函数的命名空间,在任何意义上都不是一个对象的分类。

这种将对象分类和命名空间混为一谈的做法是令人困惑的,也是不必要的。

Java(以及延伸到C#)的另一个过度是,方法默认是多态的。多态性是一种工具,而不是一种规则。许多(如果不是大多数)函数调用都不需要动态调度

这些种类的过度导致了对类的真正含义的混淆。

所以,让我们开门见山吧:

软件设计的最古老的规则之一是:我们应该将系统的元素划分为松散耦合和内部凝聚的元素(banq:低关联高内聚)。这些元素成为我们可以放置数据和行为的命名良好的地方。这种做法遵循了一个古老的谚语:每件事都有一个地方,每件事都有它的位置。

这些元素是什么?似乎很明显,对象的分类结构应该在列表中占据重要位置。像java.lang.Math这样的命名的函数库是另一个明显的选择。

  • 在一种情况下,我们有一批操纵内部数据结构的函数。
  • 在另一种情况下,我们有一批操纵外部数据结构的函数。

这些元素或这批函数的基本特征:它们都是内部凝聚的。
这意味着这批函数中的所有函数都是相互紧密联系的,因为它们操作的是相同的数据结构,无论是内部还是外部。正是这种内聚性推动了软件设计的分区。

# # # 例子
最近我在写一个叫more-speech的应用程序,它是一个浏览nostr网络上消息的客户端。这个新工作是由中继器组成的,它们使用简单的websocket协议来向客户端传输消息。more-speech客户端是用Clojure编写的,这是一种函数编程语言。

在早期,我创建了一个名为protocol的模块,以容纳实现nostr protocol 的代码。在这个模块中,我首先管理了信息所经过的websockets,然后根据协议的规则对这些信息进行解码和处理。

Clojure不是一个传统的OOPL,没有用于声明和定义对象以及操作对象的方法的类关键词。相反,Clojure中的模块只是一批没有在语法上与任何特定数据绑定的函数。因此,我的protocol 模块有处理WebSockets的函数、处理消息的函数和处理protocol 元素的函数。它们是有凝聚力的,因为它们都与nostr协议有关;但没有中心数据结构来统一它们。

有一天,我意识到我缺少一个抽象的东西。nostr protocol 确实可能会通过websockets传输的,但这个protocol 规则却与websockets毫无关系。这些规则处理的是借用websockets实现传输数据,而不是依赖websockets本身,然而,我的协议模块却充斥着websocket的代码。

因此,我通过创建一个名为relay的抽象,将websocket代码与protocol 代码分开:

  • relay是一个数据结构,它包含websocket的url 、websocket本身,以及一个在收到消息时调用的函数。
  • relay的数据结构由诸如make、open、close和send等函数来操作。

这个relay模块非常清楚地定义了一个对象的类别:
而nostr protocol 为活动中的relay列表中的每个URL构建一个中继对象,它打开这些relay并向它们发送消息,收到的消息通过传递到构建relay对象的函数中的回调函数发送至protocol 。
为了保持函数式编程的不变性和参考透明度约束,更新relay状态的函数会返回该relay的新实例。

##教训
Java、C#、Ruby和C++都强制要求或者强行鼓励将系统划分为“类class”,Clojure则不然,它对类完全不了解。

我从protocol 和relay中得到的教训是,在编写复杂的Clojure程序时,我没有对类结构给予足够的重视。相反,我一直让函数或多或少地在模块中积累,类似于用C、Fortran、Basic甚至Assembler编程的方式。

而这是一种懒惰。

对象存在于程序中,它们可以而且应该被分类。

所以,从现在开始,我将会更加关注我的系统中的对象的分类结构。

每样东西都有一个位置,每样东西都有它的位置。

原文点击标题