曾经的我和“谓词”这个词搏斗了半年

0 阅读7分钟

曾经的我和“谓词”这个词搏斗了半年

1.第一次见面,我觉得这名字有病

2017年,Java 8 普及的第二年。 我写下了人生中第一个 filter:很顺畅,很优雅,很现代。

list.stream()
.filter(item->
item.getPrice() >100)
.collect(Collectors.toList());

然后我点进了 filter 的源码。

Stream<T> filter(Predicate<?superT> predicate);

Predicate 什么玩意儿? 我右键 → Go to Definition。

@FunctionalInterface
public interface Predicate<T> {    
   boolean test(T t);
}

就这?一个接口,一个 test 方法,返回布尔值。 那为什么不叫 Checker?不叫 Matcher?不叫 Condition? 叫 Predicate——为什么呢?有逼格?

这种问题,在我一开始入门学java的时候,调用Math.random函数同样有疑问,为什么调这个方法,可以传递一个可选的参数.seed?因为认知的缺失,当时的自己绞尽脑汁都想不到为什么random和seed这个单词有什么关系)

我甚至去搜了这个词。 “谓词。语言学名词。主谓宾结构中的谓语部分。” ……更迷惑了。 一个过滤条件,跟语文有什么关系? 那一刻我给 Java 设计者打了个标签:学院派,爱起生僻名,不接地气。 这个标签,贴了半年。

2.再次相遇

我发现是自己没出过那个语境,因为后来我开始写一些稍微复杂的 Stream 需要组合条件:

Predicate<Item> isExpensive =
item->item.getPrice() >100;
Predicate<Item> isOnSale =
item->item.getDiscount() >0;
// 组合
stream.filter(isExpensive.and(isOnSale))

等等。。。 andPredicate可以and? 我点进去:

default Predicate<T> and(Predicate<?superT>other) {    
  return t->test(t) && other.test(t);
}

它返回了一个新的Predicate。 它不是一段代码。 它是一个对象——可以被传递、组合、取反、复用的逻辑单元。 那一刻我开始觉得不对劲。 如果它只是一个过滤条件,为什么要设计成可以组合? 为什么要把一个“函数”包装成一个“接口”? 他们到底在做什么?

3. Stream 源码里藏着的答案

我决定不再当使用者。 我点进了ReferencePipeline。 然后我看见了Sink。

interface Sink<T> extends Consumer<T> {
    default void begin(long size) {}
    default void end() {}
    default boolean cancellationRequested() { return false; }
}

这是一个链。 每个 Stream 操作——filter、map、sorted——都不是立刻执行的。 它们只是往这个链上挂载了一个 Sink。 filter挂载一个“只让某些元素通过”的 Sink。 map挂载一个“把元素变个样子”的 Sink。 等到collect被调用,这个链才从头到尾跑一遍。 这不是面向对象。 这是工厂流水线。 你站在流水线旁边,往不同的工位上放加工指令。 加工指令是什么? 是函数。 filter的指令:判断一下,不通过就扔掉。 map的指令:转换一下,变成另一个东西。 sorted的指令:两两比较,排个序。 JDK 的设计者,用对象搭建了一条流水线,然后用函数定义了流水线上的每一个工位。

4. 我终于看懂了“谓词”

再回头看Predicate。 它不是“过滤条件”。 不是“判断方法”。 不是“布尔表达式”。 它是逻辑学里那个“带洞的命题”。 逻辑学里,命题是有真伪的陈述: 这些简单的标记可以让你的内容更有层次感和重点突出。

今天是周三。

谓词是带变量的命题:

x 是周三。 x.isWednesday()

它不关心 x 是谁,只关心“x 是不是周三”这个判断本身。 这就是Predicate !

boolean test(T t);

你给我一个 T,我还你一个 boolean。 判断的逻辑被封装成了对象,等待你的数据来填充。

5.所以,Java 到底是面向对象还是函数式?

都是。都不是。面向对象还是函数式 这句话本身是在描述一种思想,语言只是会有倾向性,为你开发提供语法糖,看你在哪一层。 你站在流水线旁边,看整个 Stream 的骨架: Pipeline→ 类 Sink→ 接口 AbstractPipeline→ 抽象类 继承、多态、重写 这是面向对象。健壮、封闭、经得起大型工程考验。 你蹲下来,看流水线上具体在做什么: filter接受一个Predicate map接受一个Function sorted接受一个Comparator 这是函数式。抽象、泛化、把“行为”当成“值”来传递。 JDK 的设计者没有选边站。 他们用面向对象造了容器,用函数式定义了容器里的内容。

5.这个认知,被我带去了 Python,Go,Ts

后来我写 Python。 看见filter函数:

filter(lambda item: item.price > 100, items)

Python甚至不需要 Predicate 这个词。 你直接写lambda item: item.price > 100——这就是谓词,只是它没被命名,没被打包成一个显式的类型。 我以前觉得:Python 就是灵活,Java 就是啰嗦。 现在我知道:不是啰嗦,是选择。 Java 的设计者选择了把这个逻辑实体命名、装箱、类型化。 代价:你多学一个词,多写几行接口定义。收益:你可以在类型系统里操作“逻辑”本身——and、or、negate。 Python 的设计者选择了信任你。 你觉得需要给逻辑命名?你自己写 def。你觉得需要组合逻辑?你自己写lambda x: f(x) and g(x)。 没有谁对谁错。 只是两种不同的“把思维映射成代码”的方式。 而我,曾经执拗地以为方式有对错,哪个更优雅。

6. 回头看,看山还是山

现在我写 Java。 看见Predicate,不再觉得它晦涩。 我知道写下这个名字的人,可能来自一个比我更老的学术传统——数理逻辑、lambda 演算、类型论。 他觉得这个传统值得被带进编程语言。 他用一个词,给我的 IDE 开了一扇通往 1930 年代的门。 现在我写 Python。 看见 lambda,不再觉得它“不过是个匿名函数”。 我知道这是直接表达逻辑,不需要中间商赚差价。 我也知道,当这个 lambda 复杂到需要命名、需要测试、需要复用的时候——我会主动把它抽出来,写成一个普通的 def,甚至一个类。 语言能限制的,只是语法。 限制不了的是设计者脑子里那个模型。 也限制不了你脑子里的那个。

7.最后

所以,面向对象 or 面向过程 你有答案了吗? 我的答案是: 面向对象,是封装的艺术 ,对于设计者来说用来隔离代码的边界,减少使用者的认知负担。 它不是给你的代码找一个个“类”当容器——它是给你一张契约。 你写一个类,设计它的字段、方法、可见性。你痛苦地思考:哪些该暴露,哪些该藏起来,哪些该写成final不让别人动。 你不是在封装代码。 你是在给下一个接手的人画边界,是在假设这段代码别人无法更改的时候,如何便捷的扩展逻辑。 “这个模块归我管,你只许通过这几个public方法碰我。里面的烂摊子,你不用知道。” 这是设计者的修养:把复杂留给自己,把简单留给别人。 面向过程,是抽象的艺术。 它不是“把函数写在类外面”这种语法层面的小事。 是你终于把“怎么做”打包成一个值,传来传去。 Predicate、Function、Consumer——它们不是类,它们是逻辑的载体。 你写 filter(item -> item.getPrice() > 100) 的那一刻,没有new一个类,没有implement一个接口,没有写Override。 你只是说:这是判断逻辑,拿去用。 这是设计者的自由:不再被“代码必须装进类里”这个念头绑架。 所以面向对象和面向过程争了几十年,争的是什么? 争的是:我们到底应该先保护人,还是先解放代码。

结语

面向对象说:代码会变,人会走。把边界画清楚,后来者才不会掉进坑里。 面向过程说:需求会变,问题会变。把逻辑抽象出来,我才能应对我不知道的明天。 我没有答案。 我只是不再选边站了。 写框架、写中间件、写给别人用的SDK—— 我会用面向对象。画好边界,写好文档,private藏好,final锁死。 这是我的责任。 写业务、写胶水层、写一次性脚本—— 我会用函数式。传Lambda,链式调用,不新建类,不生造名词。 这是我的自由。 面向对象是你的良心。 面向过程是你的刀刃。 一个合格的程序员,两样都得带在身上。


🦓 我是斑马,一个正在通关全栈的程序员。 这条路还很长,一起走。 何不扫个码,一起码上见分晓? 下期预告: AI时代下的技术入门焦虑