Criando um Pattern Matching em C#
Olá pessoa!
Antes de qualquer coisa, sim, eu sei que existe um Pattern Matching nativo em C#, mas na minha opinião ele é relativamente estranho. Então vou propor algo um pouquinho diferente hoje. Criarmos o nosso próprio pattern matching.
Ok, talvez possa parecer um pouco ambicioso da minha parte, estou falando que o pattern matching nativo do C# é estranho e estou me proponho a fazer um diferente. Bom, na verdade, só de termos acesso ao pattern matching no C# hoje em dia já é bem legal!
Mas como vocês já devem saber, eu gosto bastante de programação funcional e depois de algumas experimentações por aqui cheguei em um resultado que fiquei satisfeito e gostaria de compartilhar com vocês (e coletar opiniões).
Primeiro vale citar uma pequena explicação sobre o pattern matching:
Como o próprio nome sugere, pattern matching é utizado para comparar padrões. Um pattern matching é uma expressão que realiza uma comparação e extração de valores. Não é exatamente isso, mas você pode entender um pattern matching como um
switch
que permite realizar comparações e retornar valor a partir de uma expressão.
Introdução
Primeiro vale uma introdução geral sobre pattern matchings em F#, se você já domina este assunto, fique à vontade para pular para a implementação proposta na próxima seção.
Vamos começar com um pattern matching “comum” em F#, algo similar à um desvio condicional, veja:
let valor = 10
let texto =
match valor with
| valor when valor % 2 = 0 -> "Numero Par"
| valor when valor > 5 -> "Maior que cinco"
| _ -> "Outros casos"
Console.WriteLine texto
Neste caso, estamos comparando o valor 10 com diversas expressões diferentes e retornando uma string em correspondente à expressão escolhida. Vale lembrar que o valor discard (_) é utilizado como o default
, ou seja, se nenhum dos casos anteriores atenderem a condição, este é o caso que será escolhido.
No exemplo acima, o valor 10 faz com que o texto retornado seja o retorno da primeira expressão: “Número par”. Ele executa a primeira expressão correspondente, mesmo que neste exemplo, o valor também seja maior do que cinco.
Se alterarmos o valor para 9, teremos como resultado: “Maior que cinco” e por fim, se alterarmos o valor para 3, teremos como resultado “Outros casos”.
Tudo tranquilo até aqui, certo?
Vamos para o segundo tipo de comparação, comparação com base em tipos. Esta comparação geralmente é utilizada em conjunto com os discriminated unions, onde você possui um valor que pode ser qualquer um dos casos, veja.
type Forma = | Quadrado | Triangulo | Circulo
let forma = Quadrado
let texto =
match forma with
| Quadrado -> "Quadrado"
| Triangulo -> "Triangulo"
| Circulo -> "Circulo"
Simples né?
Também podemos fazer isso quando precisarmos obter alguma propriedade de um tipo derivado, veja esse novo exemplo:
type PessoaFisica = { CPF: string}
type PessoaJuridica = { CNPJ: string}
type Pessoa =
| PessoaFisica of PessoaFisica
| PessoaJuridica of PessoaJuridica
Neste exemplo, temos um tipo para pessoa física e outro para pessoa jurídica. Além disso, temos um tipo para pessoa, que pode ser física ou jurídica!
Podemos utilizar este pattern matching para obter o código (CPF ou CNPJ) independente do tipo de pessoa:
let pessoa = PessoaFisica {CPF = "123321123"}
let codigo =
match pessoa with
| PessoaFisica pf -> pf.CPF
| PessoaJuridica pj -> pj.CNPJ
Por fim, temos o pattern matching para estruturas de dados, este é um caso que possui várias particularidades. A principal delas é que o próprio pattern matching separa o primeiro elemento do resto da estrutura, facilitando a implementação de funções recursivas. Infelizmente o pattern matching proposto neste post ainda não cobre esta funcionalidade.
Implementação proposta
Vamos deixar uma coisa clara, a base de inspiração dessa implementação foi o pattern matching do F#, então vamos fazer algumas comparações por aqui, usando os exemplos da introdução, beleza?
Primeiro vamos para a implementação do pattern matching propriamento dito. Na prática ele virou um método de apenas uma linha. Não é dos métodos mais simples de compreender batendo o olho, admito. Mas ficou relativamente poderoso.
Como acho que ele não vai ficar tão claro colocando a implementação completa de cara, vamos por partes.
Começando pelo retorno, vamos lá, a ideia do pattern matching é retornar qualquer coisa, afinal a expressão é passada por parâmetro, por conta disso, precisamos utilizar generics.
T Match<T>()
=> default;
Até agora criamos um método que retorna o valor padrão para qualquer tipo informado no generics, certo?
Vamos entender como funcionam as expressões que vamos utilizar no pattern matching, como eu disse antes, vamos olhar para o F# primeiro:
match valor with
| valor when valor % 2 = 0 -> "Numero Par"
| valor when valor > 5 -> "Maior que cinco"
| _ -> "Outros casos"
Cada caso, pode ser separado em dois pontos: condição e expressão. Onde a condição determina qual caso será executado e a expressão determina a função que será executada naquele caso. Como precisamos de dois valores diferentes.
O primeiro parâmetro será um bool
, ou seja, a condição para executar o caso. Enquanto o segundo parâmetro deve ser uma função que retorne T
, afinal, este é o valor que será retornado pelo Match
, quando o caso for executado, veja:
T Match<T>( bool condicao, Func<T> expressao )
=> default;
Agora vamos fazer com que a expressão seja executada caso a condiçao seja verdadeira, caso contrário, vamos continuar retornando o valor padrão:
T Match<T>( bool condicao, Func<T> expressao )
=> condicao ? expressao(): default;
Este código até vai funcionar, mas uma das bases do pattern matching é poder passarmos diversos padrões diferentes na mesma comparação, como fazemos?
Felizmente o C# possui a palavra reservada params! Com esta palavra reservada permitimos que a chamada do método possa conter virtualmente infinitos parâmetros. Estes parâmetros são agrupados em um array para o corpo interno da função.
Se você não conhece o
params
, você pode acessar a documentação oficial neste link.
No entanto, temos 2 parâmetros diferentes: a condição e a expressão. E nesse caso, cada parâmetro precisa conter este par inteiro. Para fazer isso, vamos utilizar uma tupla.
Vamos lá, então vamos refatorar o parâmetro do método Match
, para ser um params array
onde cada elemento do array é uma tupla, contendo a condição e a expressão. Vamos chamar este array de casos, afinal, cada elemento dele representará um caso diferente.
T Match<T>(params (bool? condicao, Func<T> expressao)[] casos)
Por fim, vamos implementar o corpo do método, eu prometi que era só uma linha, lembra?
O pattern matching precisa encontrar a primeira condição atendida e executar sua expressão, algo bem simples de resolvermos usando Linq:
static T Match<T>(params (bool condicao, Func<T> expressao)[] casos)
=> casos.First(caso => caso.condicao.Value)
.expressao();
Neste ponto já conseguimos utilizar o Match, no entanto, realizei um último ajuste: tornar o parâmetro que define a condição um bool nullable
e definir o caso null
como um caso para executar:
T Match<T>(params (bool? condicao, Func<T> expressao)[] casos)
=> casos.First(caso => !caso.condicao.HasValue || caso.condicao.Value)
.expressao();
Eu sei, soa bastante controverso (e nem me agrada muito), mas ele produz um resultado bastante agradável e como eu disse, logo mais já vou explicar, segura aí.
Agora vamos para a utilização desse cara! - Vamos para o primeiro do pattern matching:
Primeiro em F# novamente:
let valor = 3
let texto =
match valor with
| valor when valor % 2 = 0 -> "Numero Par"
| valor when valor > 5 -> "Maior que cinco"
| _ -> "Outros casos"
Agora usando o nosso Match em C#:
int valor = 3;
string texto =
Match(
(valor % 2 == 0, () => "Número Par"),
(valor > 5, () => "Valor maior que cinco"),
(true, () => "Outros casos")
);
Perceba que apra o caso padrão, eu precisei colocar de forma fixa o valor true
, confesso que isso não me agradou muito e este é justamente o motivo de termos alterado a condição para um tipo nullable
e estarmos validando null
como um caso verdadeiro. Com isso, podemos alterar o valor true
para default
que o resultado continuará sendo o mesmo:
int valor = 3;
string texto =
Match(
(valor % 2 == 0, () => "Número Par"),
(valor > 5, () => "Valor maior que cinco"),
(default, () => "Outros casos")
);
Legal né?
Vamos para o segundo caso, onde utilizamos formas:
type Forma = | Quadrado | Triangulo | Circulo
let forma = Quadrado
let texto =
match forma with
| Quadrado -> "Quadrado"
| Triangulo -> "Triangulo"
| Circulo -> "Circulo"
Agora em C#:
public abstract class Forma { }
public class Quadrado : Forma { }
public class Triangulo : Forma { }
public class Circulo : Forma { }
Forma forma = new Quadrado();
string texto =
Match(
(forma is Quadrado, () => "Quadrado"),
(forma is Triangulo, () => "Triangulo"),
(forma is Circulo, () => "Circulo")
);
Por fim, temos o exemplo utilizando comparação de padrão e obtendo um valor (este foi o caso que menos gostei). Primeiro em F#:
type PessoaFisica = { CPF: string}
type PessoaJuridica = {CNPJ: string}
type Pessoa =
| PessoaFisica of PessoaFisica
| PessoaJuridica of PessoaJuridica
let pessoa = PessoaFisica {CPF = "123321123"}
let codigo =
match pessoa with
| PessoaFisica pf -> pf.CPF
| PessoaJuridica pj -> pj.CNPJ
E agora em C#:
public abstract class Pessoa { }
public class PessoaFisica : Pessoa
{
public string CPF { get; set; }
}
public class PessoaJuridica : Pessoa
{
public string CNPJ { get; set; }
}
Pessoa pessoaFisica = new PessoaFisica() {CPF = "12312312" };
string codigo =
Match(
(pessoaFisica is PessoaFisica, () => pessoaFisica is PessoaFisica pf ?
pf.CPF : ""),
(pessoaFisica is PessoaJuridica, () => pessoaFisica is PessoaJuridica pj ?
pj.CNPJ : "")
);
É eu sei, ficou bastante verboso e fazemos duas comparações.
Bom pessoal, este é o estágio em que o pattern matching está, assim que eu gostar mais dele vou incluí-lo na Tango
, por enquanto, ainda fica só como experimentação!
Problemas que ainda existem:
- Não está suportando pattern matching com coleção de dados separando Head e Tail;
- Para pattern matching com classes derivadas preciamos fazer a comparação com
is
duas vezes; - Todas condições são validadas antes do Match executar (essa dá para resolver transformando o parâmetro de bool? para Func<bool?>, mas ainda não tenho certeza do quanto gosto disso)
Me mandem feedbacks, gostaram? Acharam que ficou ruim? Sugestões? Críticas?
- Bom, me conte nos comentários!
E Até mais!
Sempre vale lembrar que as informações e textos aqui no blog representam minha opinião pessoal, o que pode não ser igual à sua ou de qualquer outra pessoa, incluindo a empresa para qual eu trabalho. Portanto as publicações inseridas aqui estão relacionadas somente a mim.