Цель протоколов в 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)
Теперь, если вы хотите добавить новую операцию, скажем, проверку типов, это легко, но если вы хотите добавить новый тип узла, вы должны изменить все существующие выражения сопоставления с образцом во всех операциях.
И для типичного наивного ОО у вас есть прямо противоположная проблема: легко добавить новые типы данных, которые работают с существующими операциями (например,r путем наследования или переопределения), но трудно добавить новые операции, поскольку это в основном означает изменение существующих классов / объектов.
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
состоит из и a push
и pop
функции вместе .
Итак, почему бы не простодобавить возможность группировать мультиметоды вместе?Есть чисто прагматичная причина, и именно поэтому я использовал слово «эффективный» во вступительном предложении: производительность.
Clojure - хост-язык.Т.е. он специально разработан для работы на платформе другого языка.И оказывается, что практически любая платформа, на которой вы хотите, чтобы Clojure работала (JVM, CLI, ECMAScript, Objective-C), имеет специализированную высокопроизводительную поддержку для отправки исключительно по типу первого аргумента,Clojure Multimethods OTOH отправка по произвольным свойствам из по всем аргументам .
Итак, протоколы ограничивают вас отправкой only по first аргумент и только для его типа (или как особый случай для nil
).
Это не ограничение самой идеи протоколов, это прагматичный выборполучить доступ к оптимизации производительности базовой платформы.В частности, это означает, что протоколы имеют тривиальное отображение на интерфейсы JVM / CLI, что делает их очень быстрыми.Фактически, достаточно быстро, чтобы можно было переписать те части Clojure, которые в настоящее время написаны на Java или C # в самом Clojure.
Clojure фактически уже имеет протоколы, поскольку версия 1.0: Seq
является протоколом,например.Но до версии 1.2 вы не могли писать протоколы в Clojure, вы должны были писать их на языке хоста.