【问题标题】:Simple explanation of clojure protocolsclojure协议的简单解释
【发布时间】:2011-05-29 10:18:16
【问题描述】:

我正在尝试了解 clojure 协议以及它们应该解决的问题。有人对 clojure 协议的内容和原因有清楚的解释吗?

【问题讨论】:

标签: clojure protocols


【解决方案1】:

我发现将协议视为在概念上类似于 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

【讨论】:

  • > 就像面向对象语言中的隐式 "this" 参数我注意到传递给协议函数的 var 在 Clojure 代码中通常也称为 this
【解决方案2】:

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 有 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 pushpop 函数 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 中编写协议,而必须使用宿主语言编写它们。

【讨论】:

  • 感谢您提供如此详尽的回答,但您能否澄清您对 Ruby 的看法。我想在 Ruby 中(重新)定义任何类(例如 String、Fixnum)的方法的能力类似于 Clojure 的 defprotocol。
  • 一篇关于表达式问题和 clojure 协议的优秀文章 - ibm.com/developerworks/library/j-clojure-protocols
  • 很抱歉对这么老的答案发表评论,但您能否详细说明为什么扩展和接口(C#/Java)不是解决表达式问题的好方法?
  • Java 在此处使用该术语的意义上没有扩展。
  • Ruby 的改进使得猴子补丁过时了。
猜你喜欢
  • 2018-02-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-27
  • 2013-11-01
  • 2012-05-29
  • 1970-01-01
相关资源
最近更新 更多