关于 Clojure 协议的简单解释

我在试着理解灵术协议以及它们应该解决什么问题。有人能清楚地解释一下,什么是修行协议,为什么是修行协议吗?

19743 次浏览

我发现将协议看作是在概念上类似于 Java 等面向对象语言中的“接口”是最有帮助的。一个协议定义了一组抽象的函数,这些函数可以用给定对象的具体方式来实现。

举个例子:

(defprotocol my-protocol
(foo [x]))

定义一个包含一个名为“ foo”的函数的协议,该函数作用于一个参数“ x”。

然后,您可以创建实现该协议的数据结构,例如。

(defrecord constant-foo [value]
my-protocol
(foo [x] value))


(def a (constant-foo. 7))


(foo a)
=> 7

注意,这里实现协议的对象作为第一个参数 x传递——有点像面向对象语言中隐式的“ this”参数。

协议的一个非常强大和有用的特性是您可以将它们扩展到对象 即使对象最初并不是为支持协议而设计的。例如,您可以将上面的协议扩展到 java.lang。如果您喜欢,可以使用字符串类:

(extend-protocol my-protocol
java.lang.String
(foo [x] (.length x)))


(foo "Hello")
=> 5

在 Clojure,协议的目的是以有效的方式解决表达问题。

表达式问题是什么?它指的是可扩展性的基本问题: 我们的程序使用操作来操作数据类型。随着程序的发展,我们需要用新的数据类型和新的操作来扩展它们。特别是,我们希望能够添加与现有数据类型一起工作的新操作,并且我们希望添加与现有操作一起工作的新数据类型。我们希望这是真正的 分机,也就是说,我们不想修改 存在程序,我们想尊重现有的抽象,我们希望我们的扩展是独立的模块,在独立的名称空间,独立编译,独立部署,独立类型检查。我们希望它们是类型安全的。[注意: 不是所有语言都有意义。但是,例如,即使在像 Clojure 这样的语言中,使它们具有类型安全的目标也是有意义的。仅仅因为我们不能静态 检查完毕类型安全并不意味着我们希望我们的代码随机中断,对吗? ]

表达式问题是,实际上如何在语言中提供这种可扩展性?

事实证明,对于典型的过程和/或函数式编程的幼稚实现,添加新的操作(过程、函数)非常容易,但添加新的数据类型非常困难,因为基本上操作使用某种区分大小写的方法(switchcase、模式匹配)来处理数据类型,你需要添加新的用例,即修改现有代码:

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 有单位,Go 有接口,CLOS 和 Clojure 有 Multimethod。还有一些“解决方案”,尝试可以解决这个问题,但是会以这样或那样的方式失败: C # 和 Java 中的接口和扩展方法,Ruby 中的 Monkeypatching,Python,ECMAScript。

注意,Clojure 实际上已经有了 已经机制来解决表达式问题: Multimethod。OO 与 EP 之间的问题在于,它们将操作和类型捆绑在一起。使用 Multimethod,它们是独立的。FP 的问题在于它们将操作和案例区分捆绑在一起。同样,使用 Multimethod,它们是独立的。

因此,让我们比较协议和多方法,因为它们都做同样的事情。或者,换句话说: 为什么协议,如果我们已经 多方法?

协议比多方法提供的主要功能是分组: 您可以将多个函数分组在一起,并说“这3个函数 一起形式的协议 Foo”。使用 Multimethod 不能这样做,它们总是独立存在。例如,可以声明 Stack协议由 都有pushpop函数 一起组成。

那么,为什么不直接添加将 Multimethod 分组在一起的功能呢?这是一个纯粹的实用主义原因,这就是为什么我在我的开场白中使用了“效率”这个词: 绩效。

Clojure 是一种托管语言。也就是说,它是专门设计来运行在 另一个语言的平台之上的。事实证明,几乎所有您希望 Clojure 运行的平台(JVM、 CLI、 ECMAScript、 Objective-C)都有专门的高性能支持,可以在第一个参数的类型上分派 独自一人。Clojure 多方法 OTOH 调度在 所有的争论任意性质上的应用。

因此,协议限制您在 第一参数上分派 只有,在其类型上分派 只有(或者作为 nil上的特殊情况)。

这并不是对协议本身思想的限制,而是获取底层平台的性能优化的实用选择。特别是,它意味着协议具有到 JVM/CLI 接口的简单映射,这使它们非常快。事实上,它的速度足够快,可以重写 Clojure 中当前用 Java 或 C # 在 Clojure 本身编写的那些部分。

例如,自从版本1.0以来,Clojure 实际上已经有了协议: Seq就是一个协议。但是在1.2版之前,你不能在 Clojure 编写协议,你必须用主机语言来编写。