Clojure协议的简单说明


Answers:


284

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具有多重方法。还有“解决方案” 试图解决它,但是以一种或另一种方式失败:C#和Java中的接口和扩展方法,Ruby中的Monkeypatching,Python,ECMAScript。

请注意,Clojure实际上已经具有一种解决表达问题的机制:多重方法。OO与EP的问题在于它们将操作和类型捆绑在一起。使用Multimethods,它们是分开的。FP的问题是将操作和案例区分捆绑在一起。同样,对于多重方法,它们是分开的。

因此,让我们将协议与多方法进行比较,因为两者都做同样的事情。或者,换一种说法:如果我们已经有了多重方法,为什么要使用协议呢?

协议在多方法上提供的主要功能是分组:您可以将多个功能分组在一起,然后说“这三个功能一起形成协议Foo”。您无法使用Multimethod做到这一点,它们总是独立存在。例如,你可以声明一个Stack协议由双方pushpop功能一起

那么,为什么不只是添加将多方法组合在一起的功能呢?有一个纯粹的务实原因,这就是为什么我在介绍性句子中使用“有效”一词:性能。

Clojure是一种托管语言。即,它是专门为在另一种语言的平台上运行而设计的。事实证明,您希望Clojure在其上运行的几乎所有平台(JVM,CLI,ECMAScript,Objective-C)都具有专门针对按第一个参数的类型进行分派的专用高性能支持。Clojure Multimethods OTOH在所有参数的任意属性上调度。

因此,协议限制你派遣只能第一个参数,只有其类型(或作为一个特例nil)。

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

自1.0版以来,Clojure实际上就已经有了协议:Seq例如,它是一个协议。但是直到1.2,您无法使用Clojure编写协议,而必须使用宿主语言编写协议。


感谢您提供如此详尽的答案,但是您能否阐明您对Ruby的观点。我认为在Ruby中(重新)定义任何类(例如String,Fixnum)的方法的能力类似于Clojure的defprotocol。
2013年

3
在表达问题和Clojure的协议,一个优秀的文章- ibm.com/developerworks/library/j-clojure-protocols
navgeet

抱歉在这么老的答案上发表评论,但是您能否详细说明为什么扩展和接口(C#/ Java)不能很好地解决表达问题?
Onorio Catenacci 2014年

Java没有在此使用术语的扩展。
user100464 2014年

Ruby的改进使过时的猴子修补程序变得过时了。
Marcin Bilski

64

我发现将协议在概念上类似于面向对象的语言(例如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.String类:

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

(foo "Hello")
=> 5

1
>就像面向对象语言中的隐式“ this”参数一样,我注意到传递给协议函数的var也经常this在Clojure代码中调用。
克里斯(Kris)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.