Comprendre la correspondance de modèles nécessite d'expliquer trois parties:
- Types de données algébriques.
- Qu'est-ce que la correspondance de motifs
- Pourquoi c'est génial.
Types de données algébriques en un mot
Les langages fonctionnels de type ML vous permettent de définir des types de données simples appelés «unions disjointes» ou «types de données algébriques». Ces structures de données sont de simples conteneurs et peuvent être définies de manière récursive. Par exemple:
type 'a list =
| Nil
| Cons of 'a * 'a list
définit une structure de données de type pile. Pensez-y comme équivalent à ce 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;
}
}
}
Ainsi, les identificateurs Cons
et Nil
définissent une classe simple, où le of x * y * z * ...
définit un constructeur et certains types de données. Les paramètres du constructeur ne sont pas nommés, ils sont identifiés par position et type de données.
Vous créez des instances de votre a list
classe en tant que telles:
let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
Ce qui équivaut à:
Stack<int> x = new Cons(1, new Cons(2, new Cons(3, new Cons(4, new Nil()))));
Correspondance de motifs en quelques mots
La correspondance de modèles est une sorte de test de type. Supposons donc que nous ayons créé un objet de pile comme celui ci-dessus, nous pouvons implémenter des méthodes pour jeter un coup d'œil et faire apparaître la pile comme suit:
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"
Les méthodes ci-dessus sont équivalentes (bien que non implémentées en tant que telles) au C # suivant:
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();
}
(Presque toujours, les langages ML implémentent la correspondance de modèles sans tests de type ou casts au moment de l'exécution, donc le code C # est quelque peu trompeur.
Décomposition de la structure des données en quelques mots
Ok, revenons à la méthode peek:
let peek s =
match s with
| Cons(hd, tl) -> hd
| Nil -> failwith "Empty stack"
L'astuce consiste à comprendre que les identificateurs hd
et tl
sont des variables (errm ... puisqu'ils sont immuables, ils ne sont pas vraiment des "variables", mais des "valeurs";)). Si s
a le type Cons
, alors nous allons extraire ses valeurs du constructeur et les lier aux variables nommées hd
et tl
.
La correspondance de motifs est utile car elle nous permet de décomposer une structure de données par sa forme au lieu de son contenu . Alors imaginez si nous définissons un arbre binaire comme suit:
type 'a tree =
| Node of 'a tree * 'a * 'a tree
| Nil
Nous pouvons définir certaines rotations d'arbres comme suit:
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
(Le let rotateRight = function
constructeur est le sucre de syntaxe pour let rotateRight s = match s with ...
.)
Ainsi, en plus de lier la structure de données aux variables, nous pouvons également l'explorer. Disons que nous avons un nœud let x = Node(Nil, 1, Nil)
. Si nous appelons rotateLeft x
, nous testons x
le premier modèle, qui ne correspond pas car le bon enfant a le type Nil
au lieu de Node
. Il x -> x
passera au modèle suivant , qui correspondra à n'importe quelle entrée et le renverra sans modification.
À titre de comparaison, nous écririons les méthodes ci-dessus en C # comme suit:
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))));
}
}
Pour sérieusement.
La correspondance des motifs est géniale
Vous pouvez implémenter quelque chose de similaire à la correspondance de modèles en C # en utilisant le modèle de visiteur , mais ce n'est pas aussi flexible car vous ne pouvez pas décomposer efficacement des structures de données complexes. De plus, si vous utilisez la correspondance de motifs, le compilateur vous dira si vous avez omis un cas . C'est vraiment génial?
Pensez à la façon dont vous implémenteriez des fonctionnalités similaires en C # ou en langages sans correspondance de modèle. Pensez à la façon dont vous le feriez sans tests de test et sans cast au moment de l'exécution. Ce n'est certainement pas difficile , juste encombrant et encombrant. Et vous ne faites pas vérifier par le compilateur pour vous assurer que vous avez couvert tous les cas.
Ainsi, la correspondance de modèles vous aide à décomposer et à parcourir les structures de données dans une syntaxe très pratique et compacte, elle permet au compilateur de vérifier la logique de votre code, au moins un peu. Il vraiment est une caractéristique de tueur.