Я пытаюсь понять протоколы clojure и какую проблему они должны решить. Есть ли у кого-нибудь четкое объяснение того, что и почему протоколы закрытия?
Я пытаюсь понять протоколы clojure и какую проблему они должны решить. Есть ли у кого-нибудь четкое объяснение того, что и почему протоколы закрытия?
Ответы:
Цель протоколов в 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, вам приходилось писать их на основном языке.
Я считаю наиболее полезным думать о протоколах как о концептуально подобных «интерфейсу» в объектно-ориентированных языках, таких как 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
в коде Clojure.