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 alojamiento?
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 alojamiento?
Respuestas:
El propósito de los protocolos en Clojure es resolver el problema de expresión de manera eficiente.
Entonces, ¿cuál es el problema de expresión? Se refiere al problema básico de la extensibilidad: nuestros programas manipulan los tipos de datos mediante operaciones. A medida que nuestros programas evolucionan, necesitamos 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 esta sea una extensión verdadera , es decir, no queremos modificar la existenteprograma, queremos respetar las abstracciones existentes, queremos que nuestras extensiones sean módulos separados, en espacios de nombres separados, compilados por separado, implementados por separado, marcados por separado. Queremos que sean de tipo seguro. [Nota: no todos estos tienen sentido en todos los idiomas. Pero, por ejemplo, el objetivo de tenerlos seguros de tipo tiene sentido incluso en un lenguaje como Clojure. El hecho de que no podamos verificar estáticamente la seguridad de tipos no significa que queremos que nuestro código se rompa al azar, ¿verdad?]
El problema de expresión es, ¿cómo se proporciona realmente esa extensibilidad en un idioma?
Resulta que para implementaciones ingenuas típicas de programación procesal 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 utilizando algunos tipo de discriminación caso ( switch
, case
, búsqueda de patrones) y necesita añadir nuevos casos a ellos, 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, por ejemplo, la verificación de 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 una OO ingenua típica, tiene el problema exactamente opuesto: es fácil agregar nuevos tipos de datos que funcionan con las operaciones existentes (ya sea heredando o anulando), 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 usted hereda, anula o implementa todas las operaciones requeridas, pero agregar una nueva operación es difícil, porque necesita agregarlo a todas las clases hoja o a una clase base, modificando así las existentes. código.
Varios lenguajes tienen varias construcciones 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 tiene Multimethods. 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: métodos múltiples. El problema que OO tiene con el EP es que agrupan operaciones y tipos juntos. Con los multimetodos 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 métodos múltiples, ya que ambos hacen lo mismo. O, para decirlo de otra manera: ¿por qué protocolos si ya tenemos métodos múltiples?
Lo principal que ofrecen los Protocolos sobre los Multimetodos es la Agrupación: puede agrupar varias funciones juntas y decir "estas 3 funciones juntas forman el Protocolo Foo
". No se puede hacer eso con Multimethods, siempre se mantienen solos. Por ejemplo, se podría declarar que un Stack
Protocolo consiste tanto en una push
y una pop
función juntos .
Entonces, ¿por qué no simplemente agregar la capacidad de agrupar Multimethods? Hay una razón puramente pragmática, y es por eso que usé la palabra "eficiente" en mi oración introductoria: rendimiento.
Clojure es un lenguaje alojado. Es decir, está específicamente diseñado para ejecutarse sobre la plataforma de otro idioma. Y resulta que casi cualquier plataforma en la que le gustaría que Clojure se ejecute (JVM, CLI, ECMAScript, Objective-C) tiene soporte especializado de alto rendimiento para enviar únicamente el tipo del primer argumento. Clojure Multimethods OTOH distribuye sobre propiedades arbitrarias de todos los argumentos .
Por lo tanto, los protocolos lo restringen a enviar 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 opción pragmática para obtener acceso 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 ha tenido protocolos desde la versión 1.0: Seq
es un protocolo, por ejemplo. Pero hasta 1.2, no podía escribir protocolos en Clojure, tenía que escribirlos en el idioma del host.
Me resulta 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 determinado.
Un ejemplo:
(defprotocol my-protocol
(foo [x]))
Define un protocolo con una función llamada "foo" que actúa sobre un parámetro "x".
Luego puede crear estructuras de datos que implementen el protocolo, p. Ej.
(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 "this" en lenguajes orientados a objetos.
Una de las características muy potentes y útiles de los protocolos es que puede extenderlos a objetos incluso si el objeto no fue diseñado 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
this
en el código Clojure.