Clojure 中协议的目的是以有效的方式解决表达式问题。
那么,表达问题是什么?它指的是可扩展性的基本问题:我们的程序使用操作来操作数据类型。随着程序的发展,我们需要用新的数据类型和新的操作来扩展它们。特别是,我们希望能够添加与现有数据类型一起使用的新操作,并且我们希望添加与现有操作一起使用的新数据类型。 并且我们希望这是真的扩展,即我们不想修改现有的程序,我们想尊重现有的抽象,我们希望我们的扩展是单独的模块,在单独的命名空间中,单独编译,单独部署,单独类型检查。我们希望它们是类型安全的。 [注意:并非所有这些在所有语言中都有意义。但是,例如,即使在像 Clojure 这样的语言中,让它们类型安全的目标也是有意义的。仅仅因为我们不能静态地检查类型安全并不意味着我们希望我们的代码随机中断,对吧?]
表达式问题是,您如何在语言中实际提供这种可扩展性?
事实证明,对于过程和/或函数式编程的典型幼稚实现,添加新操作(过程、函数)非常容易,但添加新数据类型非常困难,因为基本上这些操作与数据一起工作使用某种大小写区分(switch、case、模式匹配)的类型,您需要向它们添加新的案例,即修改现有代码:
func print(node):
case node of:
AddOperator => print(node.left) + '+' + print(node.right)
NotOperator => '!' + print(node)
func eval(node):
case node of:
AddOperator => eval(node.left) + eval(node.right)
NotOperator => !eval(node)
现在,如果你想添加一个新的操作,比如类型检查,这很容易,但是如果你想添加一个新的节点类型,你必须修改所有操作中所有现有的模式匹配表达式。
对于典型的幼稚 OO,您会遇到完全相反的问题:添加与现有操作一起使用的新数据类型很容易(通过继承或覆盖它们),但很难添加新操作,因为基本上意味着修改现有的类/对象。
class AddOperator(left: Node, right: Node) < Node:
meth print:
left.print + '+' + right.print
meth eval
left.eval + right.eval
class NotOperator(expr: Node) < Node:
meth print:
'!' + expr.print
meth eval
!expr.eval
在这里,添加一个新的节点类型很容易,因为您要么继承、覆盖或实现所有必需的操作,但添加一个新的操作很难,因为您需要将它添加到所有叶类或基类,从而修改现有代码。
几种语言有几种结构来解决表达式问题:Haskell 有类型类,Scala 有隐式参数,Racket 有 Units,Go 有接口,CLOS 和 Clojure 有 Multimethods。还有一些“解决方案”尝试解决它,但以某种方式失败:C# 和 Java 中的接口和扩展方法,Ruby、Python、ECMAScript 中的 Monkeypatching。
请注意,Clojure 实际上已经具有一种解决表达式问题的机制:多方法。 OO 与 EP 的问题是它们将操作和类型捆绑在一起。使用 Multimethods 时,它们是分开的。 FP 的问题在于它们将操作和案例区分捆绑在一起。同样,对于 Multimethod,它们是分开的。
所以,让我们比较一下协议和多方法,因为它们都做同样的事情。或者,换一种说法:如果我们已经拥有多种方法,为什么还要使用协议?
Protocols 相对于 Multimethods 提供的主要功能是 Grouping:您可以将多个函数组合在一起并说“这 3 个函数 together form Protocol Foo”。你不能用 Multimethods 做到这一点,它们总是独立存在的。例如,您可以声明 Stack 协议由 both push 和 pop 函数 together 组成。
那么,为什么不添加将多方法组合在一起的功能呢?纯粹是出于务实的原因,这也是我在介绍性句子中使用“高效”一词的原因:性能。
Clojure 是一种托管语言。 IE。它专门设计用于在另一种语言的平台之上运行。事实证明,几乎任何您希望 Clojure 在其上运行的平台(JVM、CLI、ECMAScript、Objective-C)都具有专门的高性能支持,以仅在第一个类型上调度争论。 Clojure Multimethods OTOH 在所有参数的任意属性上调度。
因此,协议限制您在 first 参数上调度 only 并在其类型上调度 only (或作为@987654330 上的特殊情况@)。
这并不是对协议本身概念的限制,它是获得底层平台性能优化的务实选择。特别是,这意味着协议与 JVM/CLI 接口有一个简单的映射,这使得它们非常快。事实上,速度足够快,能够在 Clojure 本身中重写当前用 Java 或 C# 编写的 Clojure 部分。
从 1.0 版开始,Clojure 实际上已经有了协议:例如,Seq 是一个协议。但在 1.2 之前,您无法在 Clojure 中编写协议,而必须使用宿主语言编写它们。