Semplice spiegazione dei protocolli di clojure


Risposte:


284

Lo scopo dei protocolli in Clojure è di risolvere il problema di espressione in modo efficiente.

Quindi, qual è il problema di espressione? Si riferisce al problema di base dell'estensibilità: i nostri programmi manipolano i tipi di dati usando le operazioni. Man mano che i nostri programmi si evolvono, dobbiamo estenderli con nuovi tipi di dati e nuove operazioni. E in particolare, vogliamo essere in grado di aggiungere nuove operazioni che funzionano con i tipi di dati esistenti e vogliamo aggiungere nuovi tipi di dati che funzionano con le operazioni esistenti. E vogliamo che questa sia vera estensione , cioè non vogliamo modificare quella esistenteprogramma, vogliamo rispettare le astrazioni esistenti, vogliamo che le nostre estensioni siano moduli separati, in spazi dei nomi separati, compilati separatamente, distribuiti separatamente, controllati separatamente. Vogliamo che siano sicuri per il tipo. [Nota: non tutti questi hanno senso in tutte le lingue. Ma, ad esempio, l'obiettivo di renderli sicuri per i tipi ha senso anche in un linguaggio come Clojure. Solo perché non possiamo controllare staticamente la sicurezza del tipo non significa che vogliamo che il nostro codice si rompa casualmente, giusto?]

Il problema dell'espressione è, come si può effettivamente fornire tale estensibilità in una lingua?

Si scopre che per implementazioni tipiche ingenue della programmazione procedurale e / o funzionale, è molto facile aggiungere nuove operazioni (procedure, funzioni), ma è molto difficile aggiungere nuovi tipi di dati, poiché sostanzialmente le operazioni funzionano con i tipi di dati usando alcuni sorta di caso di discriminazione ( switch, case, pattern matching) e avete bisogno di aggiungere nuovi casi per loro, vale a dire modificare il codice esistente:

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)

Ora, se si desidera aggiungere una nuova operazione, ad esempio, il controllo del tipo, è facile, ma se si desidera aggiungere un nuovo tipo di nodo, è necessario modificare tutte le espressioni di corrispondenza dei pattern esistenti in tutte le operazioni.

E per la tipica OO ingenua, hai l'esatto problema opposto: è facile aggiungere nuovi tipi di dati che funzionano con le operazioni esistenti (ereditandole o sovrascrivendole), ma è difficile aggiungere nuove operazioni, dal momento che ciò significa sostanzialmente modificare classi / oggetti esistenti.

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

Qui, aggiungere un nuovo tipo di nodo è facile, perché erediti, sostituisci o implementi tutte le operazioni richieste, ma aggiungere una nuova operazione è difficile, perché è necessario aggiungerlo a tutte le classi foglia o a una classe base, modificando così codice.

Diversi linguaggi hanno diversi costrutti per risolvere il problema dell'espressione: Haskell ha le macchine da scrivere, Scala ha argomenti impliciti, Racket ha Unità, Go ha Interfacce, CLOS e Clojure hanno Multimethods. Esistono anche "soluzioni" che tentano di risolverlo, ma falliscono in un modo o nell'altro: interfacce e metodi di estensione in C # e Java, Monkeypatching in Ruby, Python, ECMAScript.

Si noti che Clojure in realtà ha già un meccanismo per risolvere il problema di espressione: multimetodi. Il problema che OO ha con l'EP è che raggruppano operazioni e tipi insieme. Con Multimethods sono separati. Il problema che FP ha è che raggruppano l'operazione e la discriminazione del caso insieme. Ancora una volta, con Multimethods sono separati.

Quindi, confrontiamo i protocolli con i metodi multipli, poiché entrambi fanno la stessa cosa. O, per dirla in altro modo: perché Protocolli se noi già abbiamo multimethods?

La cosa principale che i protocolli offrono su Multimethods è il raggruppamento: puoi raggruppare più funzioni insieme e dire "queste 3 funzioni insieme formano il protocollo Foo". Non puoi farlo con i Multimetodi, sono sempre soli. Ad esempio, è possibile dichiarare che un Stackprotocollo è composto sia da una pushche da una popfunzione insieme .

Quindi, perché non aggiungere semplicemente la possibilità di raggruppare Multimethods? C'è una ragione puramente pragmatica, ed è per questo che ho usato la parola "efficiente" nella mia frase introduttiva: performance.

Clojure è una lingua ospitata. Cioè è specificamente progettato per essere eseguito sulla piattaforma di un'altra lingua. E si scopre che praticamente qualsiasi piattaforma su cui vorresti eseguire Clojure (JVM, CLI, ECMAScript, Objective-C) ha un supporto specializzato ad alte prestazioni per l'invio esclusivamente sul tipo del primo argomento. Clojure Multimethods OTOH invia proprietà arbitrarie di tutti gli argomenti .

Quindi, i protocolli ti limitano a spedire solo sul primo argomento e solo sul suo tipo (o come caso speciale su nil).

Questa non è una limitazione all'idea dei Protocolli di per sé, è una scelta pragmatica per ottenere l'accesso alle ottimizzazioni delle prestazioni della piattaforma sottostante. In particolare, significa che i protocolli hanno una mappatura banale alle interfacce JVM / CLI, il che li rende molto veloci. Abbastanza veloce, infatti, per poter riscrivere quelle parti di Clojure che sono attualmente scritte in Java o C # nello stesso Clojure.

Clojure ha già avuto protocolli dalla versione 1.0: Seqè un protocollo, per esempio. Ma fino alla 1.2, non potevi scrivere Protocolli in Clojure, dovevi scriverli nella lingua host.


Grazie per una risposta così approfondita, ma puoi chiarire il tuo punto in merito a Ruby. Suppongo che la capacità di (ri) definire metodi di qualsiasi classe (ad esempio String, Fixnum) in Ruby sia un'analogia con il protocollo di Clojure.
defhlt

3
Un eccellente articolo sul problema delle espressioni e sui protocolli di clojure - ibm.com/developerworks/library/j-clojure-protocols
navgeet

Mi dispiace pubblicare un commento su una risposta così vecchia ma potresti approfondire il motivo per cui estensioni e interfacce (C # / Java) non sono una buona soluzione al problema delle espressioni?
Onorio Catenacci,

Java non ha estensioni nel senso che il termine è usato qui.
user100464

Ruby ha delle rifiniture che rendono obsolete le patch per le scimmie.
Marcin Bilski,

65

Trovo molto utile pensare ai protocolli come concettualmente simili a una "interfaccia" in linguaggi orientati agli oggetti come Java. Un protocollo definisce un insieme astratto di funzioni che possono essere implementate in modo concreto per un determinato oggetto.

Un esempio:

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

Definisce un protocollo con una funzione chiamata "pippo" che agisce su un parametro "x".

È quindi possibile creare strutture di dati che implementano il protocollo, ad es

(defrecord constant-foo [value]  
  my-protocol
    (foo [x] value))

(def a (constant-foo. 7))

(foo a)
=> 7

Si noti che qui l'oggetto che implementa il protocollo viene passato come primo parametro x, un po 'come il parametro implicito "questo" nei linguaggi orientati agli oggetti.

Una delle funzionalità molto potenti e utili dei protocolli è che è possibile estenderli agli oggetti anche se l'oggetto non è stato originariamente progettato per supportare il protocollo . ad esempio puoi estendere il protocollo sopra alla classe java.lang.String se ti piace:

(extend-protocol my-protocol
  java.lang.String
    (foo [x] (.length x)))

(foo "Hello")
=> 5

2
> come il parametro implicito "questo" nel linguaggio orientato agli oggetti, ho notato che il var passato alle funzioni del protocollo viene spesso chiamato anche thisnel codice Clojure.
Kris,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.