Comprender la coincidencia de patrones requiere explicar tres partes:
- Tipos de datos algebraicos.
- ¿Qué patrón de coincidencia es
- ¿Por qué es asombroso?
Tipos de datos algebraicos en pocas palabras
Los lenguajes funcionales tipo ML le permiten definir tipos de datos simples llamados "uniones disjuntas" o "tipos de datos algebraicos". Estas estructuras de datos son contenedores simples y pueden definirse recursivamente. Por ejemplo:
type 'a list =
| Nil
| Cons of 'a * 'a list
define una estructura de datos tipo pila. Piense en ello como equivalente a este 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;
}
}
}
Entonces, los identificadores Cons
y Nil
definen una clase simple simple, donde of x * y * z * ...
define un constructor y algunos tipos de datos. Los parámetros para el constructor no tienen nombre, se identifican por posición y tipo de datos.
Crea instancias de su a list
clase como tales:
let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
Que es lo mismo que:
Stack<int> x = new Cons(1, new Cons(2, new Cons(3, new Cons(4, new Nil()))));
Coincidencia de patrones en pocas palabras
La coincidencia de patrones es una especie de prueba de tipo. Entonces, digamos que creamos un objeto de pila como el anterior, podemos implementar métodos para mirar y reventar la pila de la siguiente manera:
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"
Los métodos anteriores son equivalentes (aunque no se implementan como tales) al siguiente 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();
}
(Casi siempre, los lenguajes ML implementan la coincidencia de patrones sin pruebas de tipo o conversiones en tiempo de ejecución, por lo que el código C # es algo engañoso. Dejemos de lado los detalles de implementación con un poco de agitación manual, por favor :))
Descomposición de la estructura de datos en pocas palabras
Ok, volvamos al método de mirar:
let peek s =
match s with
| Cons(hd, tl) -> hd
| Nil -> failwith "Empty stack"
El truco es comprender que los identificadores hd
y tl
son variables (errm ... ya que son inmutables, no son realmente "variables", sino "valores";)). Si s
tiene el tipo Cons
, entonces sacaremos sus valores del constructor y los vincularemos a las variables llamadas hd
y tl
.
La coincidencia de patrones es útil porque nos permite descomponer una estructura de datos por su forma en lugar de su contenido . Entonces imagine si definimos un árbol binario de la siguiente manera:
type 'a tree =
| Node of 'a tree * 'a * 'a tree
| Nil
Podemos definir algunas rotaciones de árbol de la siguiente manera:
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
(El let rotateRight = function
constructor es sintaxis de azúcar para let rotateRight s = match s with ...
).
Entonces, además de vincular la estructura de datos a las variables, también podemos profundizar en ella. Digamos que tenemos un nodo let x = Node(Nil, 1, Nil)
. Si llamamos rotateLeft x
, ponemos a prueba x
contra el primer patrón, que no coincide porque el niño tiene derecho tipo Nil
en lugar de Node
. Se moverá al siguiente patrón, x -> x
que coincidirá con cualquier entrada y lo devolverá sin modificaciones.
A modo de comparación, escribiríamos los métodos anteriores en C # como:
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))));
}
}
Por enserio.
La coincidencia de patrones es increíble
Puede implementar algo similar a la coincidencia de patrones en C # usando el patrón de visitante , pero no es tan flexible porque no puede descomponer efectivamente estructuras de datos complejas. Además, si está utilizando coincidencia de patrones, el compilador le dirá si omitió un caso . ¿Qué tan maravilloso es eso?
Piense en cómo implementaría una funcionalidad similar en C # o lenguajes sin coincidencia de patrones. Piensa en cómo lo harías sin pruebas de prueba y lanzamientos en tiempo de ejecución. Ciertamente no es difícil , solo engorroso y voluminoso. Y no tiene que verificar el compilador para asegurarse de que ha cubierto todos los casos.
Por lo tanto, la coincidencia de patrones lo ayuda a descomponer y navegar por las estructuras de datos en una sintaxis compacta muy conveniente, le permite al compilador verificar la lógica de su código, al menos un poco. Realmente es una característica asesina.