La comprensione della corrispondenza del modello richiede la spiegazione di tre parti:
- Tipi di dati algebrici.
- Che cos'è il pattern matching
- Perché è fantastico.
Tipi di dati algebrici in breve
Linguaggi funzionali di tipo ML consentono di definire tipi di dati semplici chiamati "unioni disgiunte" o "tipi di dati algebrici". Queste strutture di dati sono semplici contenitori e possono essere definite in modo ricorsivo. Per esempio:
type 'a list =
| Nil
| Cons of 'a * 'a list
definisce una struttura di dati simile a uno stack. Pensalo equivalente a questo C #:
public abstract class List<T>
{
public class Nil : List<T> { }
public class Cons : List<T>
{
public readonly T Item1;
public readonly List<T> Item2;
public Cons(T item1, List<T> item2)
{
this.Item1 = item1;
this.Item2 = item2;
}
}
}
Quindi, gli identificatori Cons
e Nil
definiscono semplice una classe semplice, dove of x * y * z * ...
definisce un costruttore e alcuni tipi di dati. I parametri per il costruttore sono senza nome, sono identificati dalla posizione e dal tipo di dati.
Crei istanze della tua a list
classe in quanto tale:
let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
Che è lo stesso di:
Stack<int> x = new Cons(1, new Cons(2, new Cons(3, new Cons(4, new Nil()))));
Combinazione di motivi in breve
La corrispondenza dei modelli è una specie di prova del tipo. Quindi diciamo che abbiamo creato un oggetto stack come quello sopra, possiamo implementare metodi per sbirciare e far apparire lo stack come segue:
let peek s =
match s with
| Cons(hd, tl) -> hd
| Nil -> failwith "Empty stack"
let pop s =
match s with
| Cons(hd, tl) -> tl
| Nil -> failwith "Empty stack"
I metodi sopra indicati sono equivalenti (sebbene non implementati come tali) al seguente C #:
public static T Peek<T>(Stack<T> s)
{
if (s is Stack<T>.Cons)
{
T hd = ((Stack<T>.Cons)s).Item1;
Stack<T> tl = ((Stack<T>.Cons)s).Item2;
return hd;
}
else if (s is Stack<T>.Nil)
throw new Exception("Empty stack");
else
throw new MatchFailureException();
}
public static Stack<T> Pop<T>(Stack<T> s)
{
if (s is Stack<T>.Cons)
{
T hd = ((Stack<T>.Cons)s).Item1;
Stack<T> tl = ((Stack<T>.Cons)s).Item2;
return tl;
}
else if (s is Stack<T>.Nil)
throw new Exception("Empty stack");
else
throw new MatchFailureException();
}
(Quasi sempre, i linguaggi ML implementano la corrispondenza dei modelli senza test di tipo o cast di runtime, quindi il codice C # è in qualche modo ingannevole. Spazzoliamo i dettagli dell'implementazione a parte agitando la mano per favore :))
Decomposizione della struttura dei dati in breve
Ok, torniamo al metodo peek:
let peek s =
match s with
| Cons(hd, tl) -> hd
| Nil -> failwith "Empty stack"
Il trucco è capire che gli identificatori hd
e tl
sono variabili (errm ... dato che sono immutabili, non sono realmente "variabili", ma "valori";)). Se s
ha il tipo Cons
, estrarremo i suoi valori dal costruttore e li assoceremo alle variabili denominate hd
e tl
.
La corrispondenza dei modelli è utile perché ci consente di scomporre una struttura di dati in base alla sua forma anziché al suo contenuto . Quindi immagina se definiamo un albero binario come segue:
type 'a tree =
| Node of 'a tree * 'a * 'a tree
| Nil
Possiamo definire alcune rotazioni dell'albero come segue:
let rotateLeft = function
| Node(a, p, Node(b, q, c)) -> Node(Node(a, p, b), q, c)
| x -> x
let rotateRight = function
| Node(Node(a, p, b), q, c) -> Node(a, p, Node(b, q, c))
| x -> x
(Il let rotateRight = function
costruttore è lo zucchero di sintassi per let rotateRight s = match s with ...
.)
Quindi oltre a associare la struttura dei dati alle variabili, possiamo anche approfondire. Diciamo che abbiamo un nodo let x = Node(Nil, 1, Nil)
. Se chiamiamo rotateLeft x
, testiamo x
contro il primo modello, che non corrisponde perché il figlio giusto ha il tipo Nil
anziché Node
. Passerà al modello successivo x -> x
, che corrisponderà a qualsiasi input e lo restituirà non modificato.
Per fare un confronto, scriveremmo i metodi sopra in C # come:
public abstract class Tree<T>
{
public abstract U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc);
public class Nil : Tree<T>
{
public override U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc)
{
return nilFunc();
}
}
public class Node : Tree<T>
{
readonly Tree<T> Left;
readonly T Value;
readonly Tree<T> Right;
public Node(Tree<T> left, T value, Tree<T> right)
{
this.Left = left;
this.Value = value;
this.Right = right;
}
public override U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc)
{
return nodeFunc(Left, Value, Right);
}
}
public static Tree<T> RotateLeft(Tree<T> t)
{
return t.Match(
() => t,
(l, x, r) => r.Match(
() => t,
(rl, rx, rr) => new Node(new Node(l, x, rl), rx, rr))));
}
public static Tree<T> RotateRight(Tree<T> t)
{
return t.Match(
() => t,
(l, x, r) => l.Match(
() => t,
(ll, lx, lr) => new Node(ll, lx, new Node(lr, x, r))));
}
}
Per seriamente.
La corrispondenza dei motivi è fantastica
Puoi implementare qualcosa di simile al pattern matching in C # usando il pattern visitatore , ma non è altrettanto flessibile perché non puoi scomporre efficacemente strutture dati complesse. Inoltre, se stai usando la corrispondenza dei modelli, il compilatore ti dirà se hai lasciato un caso . Quanto è fantastico?
Pensa a come implementeresti funzionalità simili in C # o linguaggi senza pattern matching. Pensa a come lo faresti senza test-test e cast in fase di esecuzione. Non è certamente difficile , solo ingombrante e voluminoso. E non hai il compilatore che controlla per assicurarti di aver coperto ogni caso.
Pertanto, la corrispondenza dei modelli ti aiuta a scomporre e navigare le strutture dei dati in una sintassi molto comoda e compatta, consente al compilatore di controllare la logica del tuo codice, almeno un po '. E 'davvero è una caratteristica killer.