Простое объяснение clojure протоколов

Я пытаюсь понять протоколы clojure и какую проблему они должны решить. У кого-нибудь есть четкое объяснение того, что и для чего нужно протоколам clojure?

2 ответа

Решение

Цель Protocols in 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)

Теперь, если вы хотите добавить новую операцию, скажем, проверку типов, это легко, но если вы хотите добавить новый тип узла, вы должны изменить все существующие выражения сопоставления с образцом во всех операциях.

А для типичного наивного ОО у вас есть прямо противоположная проблема: легко добавлять новые типы данных, которые работают с существующими операциями (либо наследуя, либо переопределяя их), но трудно добавлять новые операции, поскольку это в основном означает изменение существующие классы / объекты.

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 есть мультиметоды. Существуют также "решения", которые пытаются ее решить, но так или иначе терпят неудачу: интерфейсы и методы расширения в C# и Java, Monkeypatching в Ruby, Python, ECMAScript.

Обратите внимание, что в Clojure уже есть механизм для решения проблемы выражения: мультиметоды. Проблема, с которой ОО сталкивается с EP, заключается в том, что они объединяют операции и типы вместе. С мультиметодами они раздельные. Проблема, с которой сталкивается FP, состоит в том, что они связывают операции и различение дел. Опять же, с мультиметодами они разделены.

Итак, давайте сравним протоколы с мультиметодами, поскольку оба делают одно и то же. Или, иначе говоря: зачем протоколы, если у нас уже есть мультиметоды?

Главное, что предлагают протоколы вместо мультиметодов, это группировка: вы можете сгруппировать несколько функций вместе и сказать "эти 3 функции вместе образуют протокол". Foo ". Вы не можете сделать это с мультиметодами, они всегда стоят сами по себе. Например, вы можете объявить, что Stack Протокол состоит из push и pop функционировать вместе.

Итак, почему бы просто не добавить возможность группировать мультиметоды вместе? Есть чисто прагматичная причина, и именно поэтому я использовал слово "эффективный" во вступительном предложении: производительность.

Clojure - хост-язык. Т.е. он специально разработан для работы на платформе другого языка. И оказывается, что практически любая платформа, на которой вы хотите, чтобы Clojure работала (JVM, CLI, ECMAScript, Objective-C), имеет специализированную высокопроизводительную поддержку для диспетчеризации исключительно по типу первого аргумента. Clojure Multimethods OTOH рассылка по произвольным свойствам всех аргументов.

Итак, протоколы ограничивают вас отправкой только по первому аргументу и только по его типу (или как особый случай на nil).

Это не ограничение идеи протоколов как таковых, это прагматичный выбор для получения доступа к оптимизации производительности базовой платформы. В частности, это означает, что протоколы имеют тривиальное отображение на интерфейсы JVM/CLI, что делает их очень быстрыми. Фактически, достаточно быстро, чтобы можно было переписать те части Clojure, которые в настоящее время написаны на Java или C# в самом Clojure.

Clojure фактически уже имел протоколы начиная с версии 1.0: Seq это протокол, например. Но до 1.2 вы не могли писать протоколы в Clojure, вы должны были писать их на языке хоста.

Я считаю наиболее полезным думать о протоколах как о концептуально похожих на "интерфейс" в объектно-ориентированных языках, таких как 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
Другие вопросы по тегам