Modelando algoritmos complexos com enum

ricardodarocha

Ricardo da Rocha

Posted on December 24, 2022

Modelando algoritmos complexos com enum

Em Rust, os enumerados são tipos muito ricos. Neste vídeo eu dei uma pequena introdução aos enumerados em Rust. É bem comum você encontrar na internet exemplos e tutoriais sobre enumerados para modelar problemas simples. Agora eu vou dar exemplos um pouco mais avançados, mas que não são muito difíceis. Na prática, vamos aplicar a técnica de composição/decomposição usando várias camadas de enumerados.

Para ilustrar o poder dos enumerados, vamos modelar um jogo de xadrez, o que pode ser considerado um problema bastante complexo. Na verdade, nós vamos modelar apenas uma parte bem modesta do jogo, porém repleta de desafios, principalmente se você pensar numa abordagem clássica de algoritmos.

Modelando um jogo de xadrez básico

No xadrez temos seis tipos de peças, mas antes vamos começar com um objetivo bem básico: simular as cores das peças.

enum Cor {
  Brancas,
  Pretas }
Enter fullscreen mode Exit fullscreen mode

E agora nós podemos declarar as peças

#[derive(Debug, Copy, Clone, PartialEq)]
enum TipoPeca {
  Peao,
  Cavalo,
  Bispo,
  Torre,
  Dama,
  Rei }
Enter fullscreen mode Exit fullscreen mode

De cara eu já anotei uma série de macros para adicionar alguns recursos básicos para o nosso tipo, como por exemplo algumas otimizações da memória. Mas não se preocupe com isso por enquanto.

Um terceiro tipo poderia encapsular os dois primeiros. Esta é uma ótima forma de esconder a complexidade.

#[derive(Copy, Clone)]
struct Peca {
  cor: Cor,
  peca: TipoPeca 
}  
Enter fullscreen mode Exit fullscreen mode
+  #[derive(Copy, Clone)]
=  enum Cor {
=    Brancas,
=    Pretas }
Enter fullscreen mode Exit fullscreen mode

E eu posso criar qualquer peça de xadrez assim:

use Cor::*;
use TipoPeca ::*;
let peca1 = Peca {
  cor: Brancas,
  peca: Cavalo,
}
Enter fullscreen mode Exit fullscreen mode

Representando um tabuleiro

Um tabuleiro é uma matriz de 8X8 o que resulta em 64 casas. Uma casa pode estar ocupada por qualquer peça, mas uma casa também pode estar vazia. É aqui que começa a ficar interessante:

#[derive(Copy, Clone)]
enum Casa{
  Ocupada(Peca),
  Vazia
}  
Enter fullscreen mode Exit fullscreen mode

Veja que, neste caso, estamos abstraindo a seguinte condição lógica

Se a casa está ocupada, então ela possui uma Peça
Senão, a casa está vazia
Enter fullscreen mode Exit fullscreen mode

Isso é muito poderoso e pode ser explorado de uma forma muito particular.

use Casa::*;
var casa: Casa;
...
match casa {
  Ocupada(_peca) => print!("♟"),
  Vazia => print!("🔲"),
}
Enter fullscreen mode Exit fullscreen mode

Um ponto interessante é que eu posso adicionar fields para um enumerado. Desta forma eu posso identificar qual peça está ocupando aquela casa. No exemplo acima eu criei um único field do tipo Peca, mas veja que poderiam ser adicionados vários fields dentro da tupla. Também seria possível adicionar fields nomeados, usando a sintaxe de estruturas.

enum Exemplo {
  ocupada{peca: Peca, cor: Cor},
  vazia(Cor,)
}
Enter fullscreen mode Exit fullscreen mode

Adicionando comportamento

Uma das tendências da programação Rust é separar os algoritmos em duas camadas: A camada de dados e a camada de comportamento. Esta técnicas é, por vezes, chamada de programação orientada a dados. Quando criamos enumerados enum e estruturas struct em geral nós estamos modelando a camada de dados, porque estas estruturas que irão armazenar as variáveis do sistema. E então temos as traits, que permitem criar métodos abstratos, e as cláusulas impl que permitem implementar métodos para as nossas estruturas. Neste artigo nós não vamos abordar traits, porém veremos que também é possível adicionar rotinas de comportamento para enumerados.

impl TipoPeca {
  fn display(&self) -> char {
    match self {
      Peao => '🟤',
      Cavalo => '🏇',
      Bispo => '🧙',
      Torre => '🚩',
      Dama => '🌸',
      Rei => '👑'
  }
}
Enter fullscreen mode Exit fullscreen mode

Se os enumerados simulam uma máquina de estados, os métodos também podem ser compreendidos como funções que alterem o estado da variável.

impl TipoPeca {
  fn promover(&self) {
    match self {
      TipoPeca::Peao => self = TipoPecao::Dama,
      outra => panic!("🛑 Esta peça {} não pode ser promovida ", outra.display);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

A aplicação final ficaria mais ou menos assim:

use Cor::*;
use TipoPeca ::*;
use Casa::*;
let mut tabuleiro = [Vazia;64];
let torre = Peca{
  cor: Brancas,
  peca: Torre};
let bispo = Peca{
  cor: Brancas,
  peca: Bispo};
...
tabuleiro[0] = Ocupada(torre);
tabuleiro[1] = Ocupada(bispo);
for casa in tabuleiro.iter() {
match casa {
  Ocupada(p) => print!("{}", p.peca.display),
  Vazia => print!("🔲"),
  }
}
Enter fullscreen mode Exit fullscreen mode

Aqui está o código ↗ para quem quiser brincar.
Bom jogo! 🦀

Sugestões de leitura

Você pode apoiar o meu livro de Rust no Catarse

Se você gostar deste artigo, é bem provável que também goste desta incrível abordagem que Joshua Cooper sobre a criação de apis e modelagem de documentos JSON com enumerados.

💖 💪 🙅 🚩
ricardodarocha
Ricardo da Rocha

Posted on December 24, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related