clojure protocols

Explicación simple de los protocolos de clojure



protocols (2)

El propósito de los Protocolos en Clojure es resolver el problema de expresión de una manera eficiente.

Entonces, ¿cuál es el problema de expresión? Se refiere al problema básico de la extensibilidad: nuestros programas manipulan tipos de datos usando operaciones. A medida que evolucionen nuestros programas, debemos ampliarlos con nuevos tipos de datos y nuevas operaciones. Y particularmente, queremos poder agregar nuevas operaciones que funcionen con los tipos de datos existentes, y queremos agregar nuevos tipos de datos que funcionen con las operaciones existentes. Y queremos que esto sea una verdadera extensión , es decir, no queremos modificar el programa existente , queremos respetar las abstracciones existentes, queremos que nuestras extensiones sean módulos separados, en espacios de nombres separados, compilados por separado, desplegados por separado, por separado. comprobado. Queremos que sean seguros de tipo. [Nota: no todos estos tienen sentido en todos los idiomas. Pero, por ejemplo, el objetivo de tenerlos seguros de tipo de letra tiene sentido incluso en un lenguaje como Clojure. El hecho de que no podamos verificar estáticamente la seguridad de tipo no significa que queremos que nuestro código se rompa aleatoriamente, ¿verdad?]

El problema de la expresión es, ¿cómo se proporciona esa extensibilidad en un idioma?

Resulta que para implementaciones ingenuas típicas de programación procedural y / o funcional, es muy fácil agregar nuevas operaciones (procedimientos, funciones), pero es muy difícil agregar nuevos tipos de datos, ya que básicamente las operaciones funcionan con los tipos de datos usando algunas tipo de discriminación de casos ( switch , case , coincidencia de patrones) y necesita agregarles nuevos casos, es decir, modificar el código existente:

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)

Ahora, si desea agregar una nueva operación, es decir, verificar el tipo, es fácil, pero si desea agregar un nuevo tipo de nodo, debe modificar todas las expresiones de coincidencia de patrones existentes en todas las operaciones.

Y para OO ingenuo típico, tiene el problema exactamente opuesto: es fácil agregar nuevos tipos de datos que funcionen con las operaciones existentes (ya sea heredando o anulándolos), pero es difícil agregar nuevas operaciones, ya que eso básicamente significa modificar clases / objetos existentes.

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

Aquí, agregar un nuevo tipo de nodo es fácil, porque hereda, anula o implementa todas las operaciones requeridas, pero agregar una nueva operación es difícil, porque necesita agregarlo a todas las clases de hoja o a una clase base, modificando así las existentes. código.

Varios lenguajes tienen varios constructos para resolver el problema de expresión: Haskell tiene clases de tipos, Scala tiene argumentos implícitos, Racket tiene unidades, Go tiene interfaces, CLOS y Clojure tienen Multimétodos. También hay "soluciones" que intentan resolverlo, pero fallan de una forma u otra: Interfaces y métodos de extensión en C # y Java, Monkeypatching en Ruby, Python, ECMAScript.

Tenga en cuenta que Clojure en realidad ya tiene un mecanismo para resolver el problema de expresión: Multimétodos. El problema que OO tiene con el EP es que agrupan operaciones y tipos juntos. Con Multimethods están separados. El problema que tiene FP es que agrupan la operación y la discriminación de casos juntos. De nuevo, con Multimethods están separados.

Entonces, comparemos los protocolos con Multimethods, ya que ambos hacen lo mismo. O, para decirlo de otra manera: ¿Por qué Protocolos si ya tenemos Multimétodos?

Lo principal que ofrecen los Protocolos sobre Multimétodos es Agrupar: puede agrupar múltiples funciones juntas y decir "estas 3 funciones juntas forman el Protocolo Foo ". No puedes hacer eso con Multimethods, siempre se sostienen por sí mismos. Por ejemplo, podría declarar que un protocolo de Stack consiste en una función de push y otra de pop juntas .

Entonces, ¿por qué no agregar la capacidad de agrupar Multimétodos juntos? Hay una razón puramente pragmática, y es por eso que utilicé la palabra "eficiente" en mi oración introductoria: rendimiento.

Clojure es un lenguaje alojado. Es decir, está diseñado específicamente para ejecutarse sobre la plataforma de otro idioma. Y resulta que prácticamente cualquier plataforma en la que le gustaría que Clojure se ejecute (JVM, CLI, ECMAScript, Objective-C) tiene soporte de alto rendimiento especializado para despachar únicamente en el tipo del primer argumento. Clojure Multimethods OTOH dispatch en propiedades arbitrarias de todos los argumentos .

Entonces, los Protocolos lo limitan a despachar solo en el primer argumento y solo en su tipo (o como un caso especial en nil ).

Esto no es una limitación en la idea de Protocolos per se, es una elección pragmática acceder a las optimizaciones de rendimiento de la plataforma subyacente. En particular, significa que los Protocolos tienen una asignación trivial a las interfaces JVM / CLI, lo que los hace muy rápidos. Lo suficientemente rápido, de hecho, para poder reescribir aquellas partes de Clojure que actualmente están escritas en Java o C # en Clojure.

Clojure ya tiene Protocolos desde la versión 1.0: Seq es un protocolo, por ejemplo. Pero hasta 1.2, no podías escribir Protocolos en Clojure, tenías que escribirlos en el idioma del host.

Estoy tratando de entender los protocolos de Clojure y qué problema se supone que deben resolver. ¿Alguien tiene una explicación clara de qué y por qué de los protocolos de clojure?


Me parece más útil pensar que los protocolos son conceptualmente similares a una "interfaz" en lenguajes orientados a objetos como Java. Un protocolo define un conjunto abstracto de funciones que se pueden implementar de manera concreta para un objeto dado.

Un ejemplo:

(defprotocol my-protocol (foo [x]))

Define un protocolo con una función llamada "foo" que actúa sobre un parámetro "x".

A continuación, puede crear estructuras de datos que implementen el protocolo, por ejemplo

(defrecord constant-foo [value] my-protocol (foo [x] value)) (def a (constant-foo. 7)) (foo a) => 7

Tenga en cuenta que aquí el objeto que implementa el protocolo se pasa como el primer parámetro x , algo así como el parámetro implícito "este" en los lenguajes orientados a objetos.

Una de las características más potentes y útiles de los protocolos es que puede extenderlos a objetos incluso si el objeto no se diseñó originalmente para admitir el protocolo . Por ejemplo, puede extender el protocolo anterior a la clase java.lang.String si lo desea:

(extend-protocol my-protocol java.lang.String (foo [x] (.length x))) (foo "Hello") => 5