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

131

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

appshare.co
источник
7
Clojure 1.2 Протоколы за 27 минут: vimeo.com/11236603
miku
3
Очень близкой аналогией с протоколами являются трейты (миксины) в Scala: stackoverflow.com/questions/4508125/…
Василь Ременюк,

Ответы:

284

Цель протоколов в 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 имеет единицы измерения, 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 Мультиметоды OTOH отправки на произвольные свойства из всех аргументов .

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

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

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

Йорг В. Миттаг
источник
Спасибо за такой обстоятельный ответ, но не могли бы вы прояснить свою точку зрения относительно Ruby. Я полагаю, что возможность (пере) определять методы любого класса (например, String, Fixnum) в Ruby аналогична defprotocol в Clojure.
defhlt 04
3
Отличная статья о Expression Problem и Clojure - х протоколов - ibm.com/developerworks/library/j-clojure-protocols
navgeet
Извините, что оставляю комментарий к такому старому ответу, но не могли бы вы уточнить, почему расширения и интерфейсы (C # / Java) не являются хорошим решением проблемы выражения?
Онорио Катеначчи
Java не имеет расширений в том смысле, в котором здесь используется этот термин.
user100464
Ruby имеет усовершенствования, которые делают исправление обезьяньего устаревшим.
Marcin Bilski
65

Я считаю наиболее полезным думать о протоколах как о концептуально подобных «интерфейсу» в объектно-ориентированных языках, таких как 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
mikera
источник
2
> Как и неявный параметр this в объектно-ориентированном языке, я заметил, что переменная, передаваемая функциям протокола, также часто вызывается thisв коде Clojure.
Крис