ReasonML: Pattern Matching: desestruturação, switch e expressões if

Estruturas e condições em ReasonML

Esse artigo faz parte da série “O que é ReasonML?”.

Atualização 2017–12–13: Reescrita completa de como os pattern matching são introduzidos.

Nesse artigo, nós iremos conhecer três características que estão relacionadas com o pattern matching: desestruturação, swtich e expressões if.

1. De Tour: Um pouco soubre Tuples

Para ilustrar patterns e pattern matching, usaremos Tuples. Os Tuples são basicamente registros cujas partes são identificadas por posição (e não pelo nome). As partes de uma Tuple são chamadas de componentes.

Vamos criar uma Tuple no rtop:

# (true, "abc");
- : (bool, string) = (true, "abc")

O primeiro componente desta Tuple é o verdadeiro booleano, o segundo componente é a string “abc”. Consequentemente, o tipo da Tuple é (bool, string).

Vamos criar mais uma Tuple:

# (1.8, 5, ('a', 'b'));
- : (float, int, (char, char)) = (1.8, 5, ('a', 'b'))

2. Pattern Matching

Antes que possamos examinar a desestruturação, switch e if, precisamos aprender a sua base: pattern matching.

Patterns são um mecanismo de programação que ajuda no processamento de dados. Eles servem para duas finalidades:

  • Verifiqua quais dados estruturados existem.
  • Extrai partes de dados.

Isso é feito combinando padrões contra dados. Sintaticamente, os padrões funcionam da seguinte forma:

  • O ReasonML possui sintaxe para criar dados. Por exemplo: as Tuples são criadas separando dados com vírgulas e colocando o resultado entre parênteses.
  • O ReasonML possui sintaxe para processar dados. A sintaxe dos padrões reflete a sintaxe para criar dados.

Vamos começar com padrões simples que suportam as Tuples. Eles têm a seguinte sintaxe:

  • Um nome de variável é um padrão. — Exemplo: x, y, foo
  • Um dado literal é um padrão. — Exemplo: 123, “abc”, true
  • Uma tupla de padrões é um padrão. — Por exemplo: (8,x), (3.2,”abc”,true), (1, (9, foo))

O mesmo nome de variável não pode ser usado em dois locais diferentes. Ou seja, o seguinte padrão é ilegal: (x, x)

2.1 Verificações de igualdade

Os padrões mais simples não possuem variáveis. Combinar esses padrões é basicamente o mesmo que uma verificação de igualdade. Vejamos alguns exemplos:

Image for post
Image for post

Até agora, usamos o padrão para garantir que os dados tenham a estrutura esperada. Como um próximo passo, apresentamos nomes de variáveis. Aqueles que tornam as verificações estruturais mais flexíveis e nos permitem extrair dados.

2.2 Nomes variáveis em patterns

Um nome de variável corresponde a qualquer dado em sua posição e leva à criação de uma variável vinculada a esses dados.

Image for post
Image for post

O nome da variável especial _ não cria bindings de variáveis e pode ser usado várias vezes:

Image for post
Image for post

2.3 Alternativas em patterns

Vamos examinar outro recurso: dois ou mais sub-patterns separados por barras verticais formam uma alternativa. Esse pattern só acontece se um dos sub-patterns corresponder à algo. Se um nome de variável existir em um sub-pattern, ele deve sair em todos os sub-patterns.

Exemplos:

Image for post
Image for post

2.4 O operador as: vincula e combina ao mesmo tempo

Até agora, você tinha que decidir se queria vincular um pedaço de dados a uma variável ou combiná-lo através de um sub-pattern. O operador as, permite que você faça os dois: o lado esquerdo é um sub-pattern, o lado direito é o nome de uma variável à qual os dados atuais serão vinculados.

Image for post
Image for post

2.5 Existem muitas outras formas de criar patterns

O ReasonML suporta tipos de dados mais complexos do que apenas as Tuples. Por exemplo: Lists e Records. Muitos desses tipos de dados também são suportados por meio de pattern matching. Mais sobre isso nos próximos artigos.

3. Pattern Matching através de let (desestruturação)

Você pode fazer pattern matching via let. Como exemplo, vamos começar criando uma Tuple:

# let tuple = (7, 4);
let tuple: (int, int) = (7, 4);

Podemos usar pattern matching para criar as variáveis x e y e ligá-las a 7 e 4, respectivamente:

# let (x, y) = tuple;
let x: int = 7;
let y: int = 4;

A variável _ também funciona e não cria variáveis:

# let (_, y) = tuple;
let y: int = 4;
# let (_, _) = tuple;

Se não corresponder um pattern, você obtém uma exceção:

# let (1, x) = (5, 5);
Warning: this pattern-matching is not exhaustive.
Exception: Match_failure.

Recebemos dois tipos de comentários do ReasonML:

  • Em tempo de compilação: um aviso de que existem (int, int) como Tuple e que o padrão não cobre. Vamos ver o que isso significa quando aprendemos a trocar expressões.
  • Em tempo de execução: uma exceção que a correspondência falhou.

pattern matching de ramo único via let é chamada de desestruturação. A desestruturação também pode ser usada com parâmetros de função (como veremos em um próximo artigo).

4. switch

let encontra um único pattern em um grupo de dados. Com uma expressão switch, podemos tentar vários patterns. O primeiro pattern a ser encontrado, irá determinar o resultado da expressão. Como em:

switch «value» {
| «pattern1» => «result1»
| «pattern2» => «result2»
···
}

O parâmetro passa pelos ramos sequencialmente: o primeiro padrão que combinar o valor, executa a expressão associada, tornando-se o resultado da expressão do switch. Vejamos um exemplo em que o pattern matching é simples:

let value = 1;
let result = switch value {
| 1 => "one"
| 2 => "two"
};
/* result == "one" */

Se o valor do switch for mais que uma única entidade (nome da variável, nome da variável qualificado, literal, etc.), ele precisa estar entre parênteses:

let result = switch (1 + 1) {
| 1 => "one"
| 2 => "two"
};
/* result == "two" */

4.1 Avisos sobre exaustão

Quando você compila o exemplo anterior ou coloca-o no rtop, você recebe o seguinte aviso de tempo de compilação:

Warning: this pattern-matching is not exhaustive.

Isso significa: O operando 1 tem o tipo int e os ramos não cobrem todos os elementos desse tipo. Este aviso é muito útil, porque nos diz que existem casos que podemos ter perdido. Ou seja, somos avisados sobre possíveis problemas à frente. Se não houver nenhum aviso, o switch sempre terá sucesso.

Se você não resolver esse problema, o ResonML lança uma exceção de tempo de execução quando um operando não possui um ramo correspondente:

let result = switch 3 {
| 1 => "one"
| 2 => "two"
};
/* Exception: Match_failure */

Uma maneira de fazer esse aviso desaparecer é lidar com todos os elementos de um tipo. Eu descreverei sucintamente como fazer isso para tipos recursivamente definidos. Estes são definidos através de:

  • Um ou mais casos base (não recursivos).
  • Um ou mais casos recursivos.

Por exemplo, para números naturais, o caso base é zero, o caso recursivo é mais um número natural. Você pode cobrir números naturais de forma exaustiva com troca através de dois ramos, um para cada caso. Como exatamente isso funciona, será descrito em um próximo artigo.

Por enquanto, basta saber que, sempre que puder, você deve fazer uma cobertura exaustiva. Em seguida, o compilador avisa se você perdeu um caso, impedindo toda a categoria de erros.

Se a cobertura exaustiva não é uma opção, você pode introduzir um ramo catch-all. A próxima seção mostra como fazer isso.

4.2 Variáveis como pattern

O aviso sobre a exaustão desaparece se você adicionar um ramo cujo padrão é uma variável:

let result = switch 3 {
| 1 => "one"
| 2 => "two"
| x => "unknown: " ++ string_of_int(x)
};
/* result == "unknown: 3" */

Criamos a nova variável x que recebe o valor do switch. Essa nova variável pode ser usada no escopo do ramo.

Esse tipo de ramo é chamado de “catch-all”: ele vem em último lugar e é executado se todos os outros ramos falharem. Em linguagens de estilo C, ramos “catch-all” são chamados de default.

Se você quiser apenas executar algo e não quer se preocupar com o valor não encontrado, você pode usar um _:

let result = switch 3 {
| 1 => "one"
| 2 => "two"
| _ => "unknown"
};
/* result == "unknown" */

4.3 Patterns para Tuples

Vamos implementar o operador AND (&&) através de uma expressão switch:

let tuple = (true, true);let result = switch tuple {
| (false, false) => false
| (false, true) => false
| (true, false) => false
| (true, true) => true
};
/* result == true */

Este código pode ser simplificado usando um _ e uma variável:

let result = switch tuple {
| (false, _) => false
| (true, x) => x
};
/* result == true */

4.4 O operador as

O operador as também funciona em patterns no swtich:

let tuple = (8, (5, 9));
let result = switch tuple {
| (0, _) => (0, (0, 0))
| (_, (x, _) as t) => (x, t)
};
/* result == (5, (5, 9)) */

4.5 Alternativas em patterns

O uso de alternativas nos sub-patterns funciona da seguinte maneira:

switch someTuple {
| (0, 1 | 2 | 3) => "first branch"
| _ => "second branch"
};

Alternativas também podem ser usadas no nível superior:

switch "Monday" {
| "Monday"
| "Tuesday"
| "Wednesday"
| "Thursday"
| "Friday" => "weekday"
| "Saturday"
| "Sunday" => "weekend"
| day => "Illegal value: " ++ day
};
/* Result: "weekday" */

4.6 Condições em ramos

guards (condições) para ramos, são específicas do switch, eles vêm após os patterns e são precedidos pela palavra-chave when. Aqui um exemplo:

let tuple = (3, 4);
let max = switch tuple {
| (x, y) when x > y => x
| (_, y) => y
};
/* max == 4 */

O primeiro ramo é avaliado apenas se o controle x> y for verdadeiro.

5. Expressões if

As expressões if do ReasonML aparecem da seguinte forma (pode ser omitido):

if («bool») «thenExpr» else «elseExpr»;

Por exemplo:

# let bool = true;
let bool: bool = true;
# let boolStr = if (bool) "true" else "false";
let boolStr: string = "true";

Dado que os blocos de escopo também são expressões, as duas seguintes expressões if são equivalentes:

Reason # if (true) 123 else "abc";
Error: This expression has type string
but an expression was expected of type int

5.1 Omitido o ramo else

Você pode omitir o ramo else — as duas expressões a seguir são equivalentes.

if (b) expr else ()
if (b) expr

Dado que ambos os ramos devem ter o mesmo tipo, expr deve conter o tipo unit (cujo único elemento é ()).

Por exemplo, print_string() retorna () e o código a seguir funciona:

# if (true) print_string("hello\n");
hello
- : unit = ()

Em contraste, esse não funciona:

# if (true) "abc";
Error: This expression has type string
but an expression was expected of type unit

6. O operador ternário ( _ ? _ :_ )

ReasonML também lhe dá o operador ternário, como uma alternativa às expressões if. As duas expressões a seguir são equivalentes.

if (b) expr1 else expr2
b ? expr1 : expr2

As duas expressões a seguir são equivalentes também.

switch (b) {
| true => expr1
| false => expr2
};
b ? expr1 : expr2;

Eu não acho o operador ternário muito útil em ReasonML: seu propósito em linguagens com sintaxe C é ter uma versão de expressão da instrução if. Mas if já é uma expressão no ReasonML.

Créditos

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store