Una cosa di cui spesso non si parla è il pattern Visitor, che consente di scegliere da quale lato del problema di espressione si vuole affrontare.
Quindi, qual è il problema dell'espressione? Si riferisce al problema di base dell'estensibilità: i nostri programmi manipolano i tipi di dati utilizzando le operazioni. Con l'evolversi dei nostri programmi, è necessario estenderli con nuovi tipi di dati e nuove operazioni. 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 funzionino con le operazioni esistenti. E vogliamo che questa sia la vera estensione , cioè non vogliamo modificare il programma esistente , 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.
Il problema dell'espressione è, in che modo puoi fornire tale estensibilità in una lingua?
Si scopre che per le tipiche implementazioni ingenue della programmazione procedurale e / o funzionale, è molto facile aggiungere nuove operazioni (procedure, funzioni), ma molto difficile aggiungere nuovi tipi di dati, poiché fondamentalmente le operazioni funzionano con i dati tipi che usano una sorta di discriminazione del caso ( switch
, case
, pattern matching) e devi aggiungere nuovi casi, cioè 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, la verifica del tipo, è facile, ma se si desidera aggiungere un nuovo tipo di nodo, è necessario modificare tutte le espressioni di corrispondenza del modello esistenti in tutte le operazioni.
E per il tipico OO ingenuo, hai il problema esattamente opposto: è facile aggiungere nuovi tipi di dati che funzionano con le operazioni esistenti (ereditandole o sovrascrivendole), ma è difficile aggiungere nuove operazioni, dal momento che fondamentalmente significa 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
In questo caso, aggiungere un nuovo tipo di nodo è facile, perché erediti, esegui l'override o implementa tutte le operazioni richieste, ma l'aggiunta di una nuova operazione è difficile, perché devi aggiungerla a tutte le classi foglia o a una classe base, modificando così il codice esistente.
Diverse lingue hanno diversi costrutti per risolvere il Problema di Espressione: Haskell ha typeclass, Scala ha argomenti impliciti, Racket ha Unità, Go ha Interfacce, CLOS e Clojure hanno Multimethods.
Tuttavia, in un linguaggio OO che non ha un modo di risolvere il Problema di Espressione (come Java o C #), il Pattern Visitatore ti consente almeno di "scegliere il tuo veleno". Quello che fa il modello è trasformare il tuo disegno di 90 ° di lato: le operazioni diventano classi ( PrintVisitor
, EvalVisitor
) e viceversa, i tipi diventa metodi ( visitAddOperator
, visitNotOperator
(o solo visit
, se la tua lingua supporta l'overload basato su argomenti)). Questo non risolve il Problema di Espressione (cioè come semplificare l'aggiunta di entrambi tipi e operazioni), ma fa ti permette di scegliere quale per semplificare.
Quindi, se la tua lingua supporta un modo per risolvere il problema dell'espressione, non hai bisogno di questa soluzione alternativa.
Nota, tuttavia, questa non è l'unica cosa che fa il pattern Visitor.
Nota: noterai la notevole assenza di qualsiasi menzione del C ++, qualunque sia. Sfortunatamente, semplicemente non ne so abbastanza. Sospetto che tra il sovraccarico e la distribuzione basata sull'argomento, l'ereditarietà virtuale, le funzioni libere, le macro e soprattutto la metaprogrammazione del modello in fase di compilazione, il Problema di espressione sia risolto in C ++, ma non lo so di sicuro.
Il problema è che una volta che qualcuno trova una soluzione per il Problema dell'espressione, lo ridefinisce per renderlo ancora più difficile, in modo da risolvere, in modo che le nuove soluzioni siano ancora più potenti ed espressive. Ad esempio, la formulazione originale della comunità Haskell non richiedeva un controllo di tipo modulare, ma la comunità di Scala ha proposto che il Problema dell'espressione non includesse solo l'estensione modulare (compilazione separata, ecc.) Di tipi e operazioni, ma anche la tipizzazione modulare e l'inferenza di tipo di quelle estensioni, che al momento sono qualcosa che solo gli impliciti di Scala possono fare e le typeclass di Haskell e i funtori di ML non possono.