Para entender a correspondência de padrões, é necessário explicar três partes:
- Tipos de dados algébricos.
- O que é a correspondência de padrões
- Por que é incrível.
Tipos de dados algébricos em poucas palavras
As linguagens funcionais do tipo ML permitem definir tipos de dados simples chamados "uniões disjuntas" ou "tipos de dados algébricos". Essas estruturas de dados são contêineres simples e podem ser definidas recursivamente. Por exemplo:
type 'a list =
| Nil
| Cons of 'a * 'a list
define uma estrutura de dados do tipo pilha. Pense nisso 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;
}
}
}
Portanto, os identificadores Cons
e Nil
definem uma classe simples e simples, onde of x * y * z * ...
define um construtor e alguns tipos de dados. Os parâmetros para o construtor não têm nome, são identificados por posição e tipo de dados.
Você cria instâncias da sua a list
classe como tal:
let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
Qual é o mesmo que:
Stack<int> x = new Cons(1, new Cons(2, new Cons(3, new Cons(4, new Nil()))));
Correspondência de padrões em poucas palavras
A correspondência de padrões é um tipo de teste de tipo. Então, digamos que criamos um objeto de pilha como o descrito acima, podemos implementar métodos para espiar e exibir a pilha da seguinte maneira:
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"
Os métodos acima são equivalentes (embora não implementados como tais) aos seguintes 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();
}
(Quase sempre, as linguagens ML implementam a correspondência de padrões sem testes de tipo ou conversão em tempo de execução; portanto, o código C # é um tanto enganador. Vamos deixar de lado os detalhes da implementação com alguns acenos de mão :))
Decomposição da estrutura de dados em poucas palavras
Ok, vamos voltar ao método peek:
let peek s =
match s with
| Cons(hd, tl) -> hd
| Nil -> failwith "Empty stack"
O truque é entender que os identificadores hd
e tl
são variáveis (errm ... já que são imutáveis, na verdade não são "variáveis", mas "valores";)). Se s
tiver o tipo Cons
, retiraremos seus valores do construtor e os vincularemos às variáveis nomeadas hd
e tl
.
A correspondência de padrões é útil porque nos permite decompor uma estrutura de dados por sua forma, em vez de seu conteúdo . Imagine se definirmos uma árvore binária da seguinte maneira:
type 'a tree =
| Node of 'a tree * 'a * 'a tree
| Nil
Podemos definir algumas rotações em árvore da seguinte maneira:
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
(O let rotateRight = function
construtor é açúcar de sintaxe para let rotateRight s = match s with ...
.)
Portanto, além de vincular a estrutura de dados às variáveis, também podemos fazer uma busca detalhada. Digamos que temos um nó let x = Node(Nil, 1, Nil)
. Se chamarmos rotateLeft x
, testamos x
o primeiro padrão, que falha ao corresponder porque o filho certo tem o tipo em Nil
vez de Node
. Ele x -> x
passará para o próximo padrão, que corresponderá a qualquer entrada e a retornará sem modificação.
Para comparação, escreveríamos os métodos acima em 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))));
}
}
Para serio.
A correspondência de padrões é incrível
Você pode implementar algo semelhante à correspondência de padrões em C # usando o padrão de visitante , mas não é tão flexível porque você não pode decompor efetivamente estruturas de dados complexas. Além disso, se você estiver usando correspondência de padrões, o compilador informará se você deixou um caso de fora . Quão incrível é isso?
Pense em como você implementaria funcionalidades semelhantes em C # ou idiomas sem correspondência de padrões. Pense em como você faria isso sem testes de teste e transmissões em tempo de execução. Certamente não é difícil , apenas pesado e volumoso. E você não tem a verificação do compilador para garantir que cobriu todos os casos.
Portanto, a correspondência de padrões ajuda a decompor e navegar nas estruturas de dados em uma sintaxe muito conveniente e compacta, permitindo que o compilador verifique a lógica do seu código, pelo menos um pouco. Realmente é uma característica do assassino.