clojure - clojure协议(protocol)的简单解释

标签 clojure protocols

我试图了解 clojure 协议(protocol)以及它们应该解决什么问题。有没有人对 clojure 协议(protocol)的内容和原因有明确的解释?

最佳答案

Clojure 中协议(protocol)的目的是以有效的方式解决表达问题。

那么,表达问题是什么?它指的是可扩展性的基本问题:我们的程序使用操作来操作数据类型。随着我们的程序的发展,我们需要用新的数据类型和新的操作来扩展它们。特别是,我们希望能够添加适用于现有数据类型的新操作,并且我们希望添加适用于现有操作的新数据类型。我们希望这是真正的扩展,即我们不想修改现有的程序,我们希望尊重现有的抽象,我们希望我们的扩展是单独的模块,在单独的命名空间中,单独编译,单独部署,单独类型检查。我们希望它们是类型安全的。 [注意:并非所有这些在所有语言中都有意义。但是,例如,即使在像 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 有接口(interface),CLOS 和 Clojure 有 Multimethods。还有一些“解决方案”试图解决它,但以一种或另一种方式失败:C# 和 Java 中的接口(interface)和扩展方法,Ruby、Python、ECMAScript 中的 Monkeypatching。

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

因此,让我们将协议(protocol)与多方法进行比较,因为两者都做同样的事情。或者,换句话说:如果我们已经有了 Multimethods,为什么还要使用 Protocols?

协议(protocol)通过 Multimethods 提供的主要功能是分组:您可以将多个函数组合在一起并说“这 3 个函数一起形成协议(protocol) Foo”。你不能用 Multimethods 做到这一点,它们总是独立存在。例如,您可以声明 Stack协议(protocol)由 push 组成和一个 pop共同发挥作用。

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

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

因此,协议(protocol)限制您仅在第一个参数及其类型上调度(或作为 nil 的特例)。

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

从 1.0 版开始,Clojure 实际上已经有了协议(protocol):Seq例如,是一个协议(protocol)。但是在 1.2 之前,你不能用 Clojure 编写协议(protocol),你必须用宿主语言编写它们。

关于clojure - clojure协议(protocol)的简单解释,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/4509782/

相关文章:

clojure - 我在 clojure 中对这个 SICP 问题做错了什么?

clojure - 在 Clojure 中使用引用

Clojure:命名空间元数据

macros - 用 Clojure + Midje 重新定义宏操作

ios - 如何将通用(关联类型需要)协议(protocol)作为委托(delegate)给任何 Controller ?

swift - 我可以在协议(protocol)中有一个初始化函数吗?

ios - '#selector' 的参数未引用初始化程序或方法

network-programming - 如何对应用程序的协议(protocol)进行逆向工程?

shell - 使用钉枪加快 clojure 启动时间

swift - 遵守协议(protocol)(作为协议(protocol))