Подтвердить что ты не робот

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

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

4b9b3361

Ответ 1

Цель протоколов в 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)

Теперь, если вы хотите добавить новую операцию, скажем, проверку типов, это просто, но если вы хотите добавить новый тип 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

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

Несколько языков имеют несколько конструкций для решения проблемы выражения: у Haskell есть классы, Scala имеет неявные аргументы, у Racket есть Units, Go имеет интерфейсы, CLOS и Clojure имеют Multimethods. Существуют также "решения", которые пытаются его решить, но так или иначе не работают: интерфейсы и методы расширения в С# и Java, Monkeypatching в Ruby, Python, ECMAScript.

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

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

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

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

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

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

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

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

Ответ 2

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