Um den Mustervergleich zu verstehen, müssen drei Teile erklärt werden:
- Algebraische Datentypen.
- Was ist Mustervergleich?
- Warum ist es so toll?
Algebraische Datentypen auf den Punkt gebracht
Mit ML-ähnlichen Funktionssprachen können Sie einfache Datentypen definieren, die als "disjunkte Vereinigungen" oder "algebraische Datentypen" bezeichnet werden. Diese Datenstrukturen sind einfache Container und können rekursiv definiert werden. Beispielsweise:
type 'a list =
| Nil
| Cons of 'a * 'a list
definiert eine stapelartige Datenstruktur. Betrachten Sie es als äquivalent zu diesem 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;
}
}
}
Die Bezeichner Cons
und Nil
definieren einfach eine einfache Klasse, wobei die of x * y * z * ...
einen Konstruktor und einige Datentypen definiert. Die Parameter für den Konstruktor sind unbenannt und werden durch Position und Datentyp identifiziert.
Sie erstellen Instanzen Ihrer a list
Klasse als solche:
let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
Welches ist das gleiche wie:
Stack<int> x = new Cons(1, new Cons(2, new Cons(3, new Cons(4, new Nil()))));
Mustervergleich auf den Punkt gebracht
Pattern Matching ist eine Art Typprüfung. Nehmen wir also an, wir haben ein Stapelobjekt wie das oben beschriebene erstellt. Wir können Methoden implementieren, um den Stapel wie folgt zu betrachten und zu öffnen:
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"
Die obigen Methoden entsprechen (obwohl nicht als solche implementiert) dem folgenden 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();
}
(Fast immer implementieren ML-Sprachen Pattern Matching ohne Laufzeit-Typentests oder Casts, daher täuscht der C # -Code etwas. Lassen Sie uns die Implementierungsdetails mit ein paar Handbewegungen beiseite schieben :))
Datenstrukturzerlegung auf den Punkt gebracht
Ok, kehren wir zur Peek-Methode zurück:
let peek s =
match s with
| Cons(hd, tl) -> hd
| Nil -> failwith "Empty stack"
Der Trick besteht darin zu verstehen, dass die Bezeichner hd
und tl
Bezeichner Variablen sind (ähm ... da sie unveränderlich sind, sind sie nicht wirklich "Variablen", sondern "Werte";)). Wenn s
der Typ vorhanden ist Cons
, ziehen wir seine Werte aus dem Konstruktor heraus und binden sie an die Variablen mit dem Namen hd
undtl
.
Der Mustervergleich ist nützlich, da wir damit eine Datenstruktur anhand ihrer Form anstelle ihres Inhalts zerlegen können . Stellen Sie sich also vor, wir definieren einen Binärbaum wie folgt:
type 'a tree =
| Node of 'a tree * 'a * 'a tree
| Nil
Wir können einige Baumrotationen wie folgt definieren:
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
(Der let rotateRight = function
Konstruktor ist Syntaxzucker für let rotateRight s = match s with ...
.)
Wir können also nicht nur die Datenstruktur an Variablen binden, sondern auch einen Drilldown durchführen. Nehmen wir an, wir haben einen Knoten let x = Node(Nil, 1, Nil)
. Wenn wir aufrufen rotateLeft x
, testen wir anhand x
des ersten Musters, das nicht übereinstimmt, weil das richtige Kind den Typ Nil
anstelle von hat Node
. Es wird zum nächsten Muster übergehen,x -> x
das mit jeder Eingabe übereinstimmt und unverändert zurückgegeben wird.
Zum Vergleich würden wir die obigen Methoden in C # schreiben als:
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))));
}
}
Für ernst.
Pattern Matching ist fantastisch
Sie können mithilfe des Besuchermusters etwas Ähnliches wie den Mustervergleich in C # implementieren , es ist jedoch bei weitem nicht so flexibel, da Sie komplexe Datenstrukturen nicht effektiv zerlegen können. Wenn Sie den Mustervergleich verwenden, teilt Ihnen der Compiler außerdem mit, ob Sie einen Fall ausgelassen haben . Wie großartig ist das?
Überlegen Sie, wie Sie ähnliche Funktionen in C # oder Sprachen ohne Mustervergleich implementieren würden. Überlegen Sie, wie Sie es ohne Test-Tests und Casts zur Laufzeit machen würden. Es ist sicherlich nicht schwer , nur umständlich und sperrig. Und der Compiler überprüft nicht, ob Sie jeden Fall abgedeckt haben.
Der Pattern Matching hilft Ihnen also, Datenstrukturen in einer sehr praktischen, kompakten Syntax zu zerlegen und zu navigieren. So kann der Compiler die Logik Ihres Codes zumindest ein wenig überprüfen . Es ist wirklich ist ein Killer - Feature.