Explicação simples dos protocolos de clojure


Respostas:


284

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 Stackprotocolo consiste em ambos um pushe uma popfunçã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.


Obrigado por uma resposta tão completa, mas você pode esclarecer seu ponto de vista sobre Ruby. Suponho que a capacidade de (re) definir métodos de qualquer classe (por exemplo, String, Fixnum) em Ruby seja uma analogia ao defprotocolo de Clojure.
defhlt

3
Um excelente artigo sobre a expressão protocolos Problema e clojure'S - ibm.com/developerworks/library/j-clojure-protocols
navgeet

Desculpe postar um comentário sobre uma resposta tão antiga, mas você pode explicar por que extensões e interfaces (C # / Java) não são uma boa solução para o problema de expressão?
Onorio Catenacci

Java não possui extensões no sentido de que o termo é usado aqui.
usar o seguinte comando

Ruby tem refinamentos que tornam obsoletos os remendos de macacos.
Marcin Bilski

64

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

1
> como o parâmetro implícito "this" na linguagem orientada a objetos, notei que o var passado para as funções de protocolo também é chamado também thisno código Clojure.
Kris
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.