Estou tentando entender os protocolos de clojure e que problema eles devem resolver. Alguém tem uma explicação clara do que é e por que dos protocolos de clojure?
Estou tentando entender os protocolos de clojure e que problema eles devem resolver. Alguém tem uma explicação clara do que é e por que dos protocolos de clojure?
Respostas:
O objetivo dos protocolos no Clojure é resolver o problema de expressão de maneira eficiente.
Então, qual é o problema da expressão? Refere-se ao problema básico de extensibilidade: nossos programas manipulam tipos de dados usando operações. À medida que nossos programas evoluem, precisamos estendê-los com novos tipos de dados e novas operações. E, particularmente, queremos poder adicionar novas operações que funcionem com os tipos de dados existentes e queremos adicionar novos tipos de dados que funcionem com as operações existentes. E queremos que essa seja uma extensão verdadeira , ou seja, não queremos modificar a existenteprograma, queremos respeitar as abstrações existentes, queremos que nossas extensões sejam módulos separados, em namespaces separados, compilados separadamente, implantados separadamente e verificados separadamente. Queremos que eles sejam seguros para o tipo. [Nota: nem todos fazem sentido em todos os idiomas. Mas, por exemplo, o objetivo de torná-los seguros para o tipo faz sentido, mesmo em um idioma como o Clojure. Só porque não podemos verificar estaticamente a segurança de tipo não significa que queremos que nosso código seja quebrado aleatoriamente, certo?]
O problema da expressão é: como você realmente oferece essa extensibilidade em um idioma?
Acontece que, para implementações ingênuas típicas de programação procedural e / ou funcional, é muito fácil adicionar novas operações (procedimentos, funções), mas muito difícil adicionar novos tipos de dados, pois basicamente as operações funcionam com os tipos de dados usando algumas tipo de caso de discriminação ( switch
, case
, correspondência de padrão) e você precisa adicionar novos casos para eles, ou seja, modificar o 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)
Agora, se você deseja adicionar uma nova operação, digamos, verificação de tipo, isso é fácil, mas se você deseja adicionar um novo tipo de nó, é necessário modificar todas as expressões de correspondência de padrões existentes em todas as operações.
E para OO ingênuo típico, você tem o problema exatamente oposto: é fácil adicionar novos tipos de dados que funcionam com as operações existentes (herdando ou substituindo-os), mas é difícil adicionar novas operações, pois isso significa basicamente modificar classes / 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
Aqui, a adição de um novo tipo de nó é fácil, porque você herda, substitui ou implementa todas as operações necessárias, mas a adição de uma nova operação é difícil, porque você deve adicioná-lo a todas as classes folha ou a uma classe base, modificando as existentes código.
Várias linguagens têm várias construções para solucionar o Problema da Expressão: Haskell possui classes tipográficas, Scala possui argumentos implícitos, Racket possui Units, Go possui Interfaces, CLOS e Clojure possuem Multimethods. Existem também "soluções" que tentam resolvê-lo, mas falham de uma maneira ou de outra: Interfaces e Métodos de Extensão em C # e Java, Monkeypatching em Ruby, Python, ECMAScript.
Observe que, na verdade, o Clojure já possui um mecanismo para solucionar o Problema da Expressão: Métodos Multimídia. O problema que o OO tem com o EP é que eles agrupam operações e tipos. Com os métodos multimídia, eles são separados. O problema que o FP tem é que eles agrupam a operação e a discriminação de casos. Novamente, com os métodos multimídia, eles são separados.
Então, vamos comparar Protocolos com Multimétodos, já que ambos fazem a mesma coisa. Ou, de outra forma: por que protocolos se já temos métodos multimídia?
Os principais protocolos coisa que oferecem mais de multimétodos é agrupar: você pode agrupar vários funções juntos e dizer "estas 3 funções em conjunto formam Protocolo Foo
". Você não pode fazer isso com os métodos multimídia, eles sempre são independentes. Por exemplo, você poderia declarar que um Stack
protocolo consiste em ambos um push
e uma pop
função em conjunto .
Então, por que não adicionar a capacidade de agrupar métodos multimídia? Há uma razão puramente pragmática, e é por isso que usei a palavra "eficiente" em minha frase introdutória: desempenho.
Clojure é um idioma hospedado. Ou seja, ele foi projetado especificamente para ser executado na plataforma de outro idioma. E acontece que praticamente qualquer plataforma na qual você deseja que o Clojure execute (JVM, CLI, ECMAScript, Objective-C) possui suporte de alto desempenho especializado para despachar apenas o tipo do primeiro argumento. Clojure Multimethods OTOH despacha em propriedades arbitrárias de todos os argumentos .
Portanto, os protocolos restringem você a enviar apenas no primeiro argumento e apenas em seu tipo (ou como um caso especial emnil
).
Isso não é uma limitação à idéia de protocolos em si, é uma opção pragmática para obter acesso às otimizações de desempenho da plataforma subjacente. Em particular, isso significa que os protocolos têm um mapeamento trivial para as interfaces JVM / CLI, o que os torna muito rápidos. Com bastante rapidez, de fato, é possível reescrever as partes do Clojure que atualmente são escritas em Java ou C # no próprio Clojure.
Clojure já possui protocolos desde a versão 1.0: Seq
é um protocolo, por exemplo. Mas, até a versão 1.2, não era possível escrever protocolos no Clojure, era preciso escrevê-los no idioma do host.
Acho mais útil pensar em protocolos como sendo conceitualmente semelhantes a uma "interface" em linguagens orientadas a objetos, como Java. Um protocolo define um conjunto abstrato de funções que podem ser implementadas de maneira concreta para um determinado objeto.
Um exemplo:
(defprotocol my-protocol
(foo [x]))
Define um protocolo com uma função chamada "foo" que atua em um parâmetro "x".
Você pode criar estruturas de dados que implementam o protocolo, por exemplo
(defrecord constant-foo [value]
my-protocol
(foo [x] value))
(def a (constant-foo. 7))
(foo a)
=> 7
Observe que aqui o objeto que implementa o protocolo é passado como o primeiro parâmetro x
- um pouco como o parâmetro implícito "this" nas linguagens orientadas a objetos.
Um dos recursos muito poderosos e úteis dos protocolos é que você pode estendê-los aos objetos, mesmo que o objeto não tenha sido originalmente projetado para suportar o protocolo . por exemplo, você pode estender o protocolo acima para a classe java.lang.String, se desejar:
(extend-protocol my-protocol
java.lang.String
(foo [x] (.length x)))
(foo "Hello")
=> 5
this
no código Clojure.