With

Eu realmente acho interessante a declaração with. Ele deixa as coisas mais claras e podemos também controlar o fluxo do código. Ela é baseada na utilização de funções encadeadas que podemos controlar em caso de sucesso e em caso de algo der errado.

Diferente dos pipes, com with podemos ter um maior controle na mesma camada de onde ele estÔ sendo implementado. Podemos definir o que esperamos de cada função por meio de pattern matching e como iremos usar a resposta nas funções subsequentes. Também podemos definir qual o fluxo ele vai tomar em caso de um erro especifico ou genérico.

A estrutura Ć© simples:

  • Declaração with

with
  • Execução de funƧƵes separadas por , (virgula). Prestando atenção que temos os valores retornados e isso Ć© de extrema importĆ¢ncia. Aqui podemos criar um controle de como serĆ” executado, a ordem de execução e critĆ©rios de aceite.

with valor_1 <- funcao_1(),
     valor_2 <- funcao_2()
  • Declaração do, onde abrimos escopo para quando tudo der certo, ele farĆ”. Muito usado para definir a resposta de uma função onde o with esta sendo composto.

with valor_1 <- funcao_1(),
     valor_2 <- funcao_2() do
  # caso tudo funciona
end
  • Declaração else, para quando algo sair errado. Aqui podemos usar a regra do pattern matching para conseguir a resposta do erro.

with valor_1 <- funcao_1(),
     valor_2 <- funcao_2() do
  # caso tudo funciona
else
  # caso algo de errado
  error -> IO.inspect(error)
end

A funcao_1 e funcao_2 são funções que estao compondo uma funcionalidade. Podemos utilizar vÔrias funções para compor uma funcionalidade, mas lembrando que nem sempre ter muitas facilita o entendimento.

circle-exclamation

Exemplo

Nada como um exemplo real para entender coisas complexas. Vamos imaginar que precisamos criar um usuƔrio. Para essa funcionalidade precisamos:

  • Validar se os dados estĆ£o corretos;

  • Salvar no banco de dados

  • Atualizar usuĆ”rio para ativo

  • Responder que tudo deu certo

São vÔrios passos. Você pode perceber que temos bem definido uma sequencia de operações até finalizar a funcionalidade. Vamos começar escrevendo o nosso teste.

Precisamos criar o modulo e a função. Começamos simples:

Primeira iteração passando, vamos lidar a primeira etapa, com a validação. Ela serÔ simples, precisamos ter somente o elemento name dentro de nosso map. Criaremos um teste para isso

Ao rodar o teste, vemos que nosso pattern matching não funcionou. Nosso teste esta esperando um {:error, reason} e recebemos um {:ok, data}. Claramente por que estamos passando um dado estÔtico. Vamos arrumar isso.

Criamos uma função dentro de users.ex para validar se os dados estão corretos.

circle-info

Temos apenas uma função, ainda não temos a necessidade de utilizar a declaração with, não ganhamos nada com isso.

Vamos atualizar o arquivo lib/users.ex

Aqui precisamos de dois tipos de resposta para nosso create/1. Quando temos sucesso e voltamos um {:ok, data} e quando de erro um {:error, reason} para assim podemos fazer o teste passar.

Temos algumas declaraƧƵes condicionais boas para isso. Iremos utilizar case para facilitar o entendimento.

Se rodar isso, teremos um bom resultado.

Aqui precisamos de uma pausa para uma reflexão. Se você analisar o código, estÔ bagunçado. Temos indentações demais, que dificultam a leitura. Vamos refatorar.

circle-info

Refatoração

Processo de modificar um sistema de software para melhorar a estrutura interna do código sem alterar seu comportamento externo.

Mais sobrearrow-up-right

Uma boa regra da refatoração é nunca mudar o comportamento do que se refatora. Isso faz com que nossos testes criados até então, sirvam de checkpoint para saber se tudo esta funcionando como deveria após a alteração.

Vamos fazer o seguinte:

Onde adicionamos o dado name vamos por para dentro de uma função chamada build/1 onde receberÔ o usuÔrio que queremos construir. Ele IrÔ montar o campo e retornar o novo valor do map.

Vamos lĆ”

Ficou melhor de ler. Ainda temos alguns problemas ali, mas vamos seguir e esperar doer mais um pouco para entender o que esta acontecendo.

Em nossa lista o próximo seria salvar o usuÔrio no banco.

  • Validar se os dados estĆ£o corretos;

  • Salvar no banco de dados

  • Atualizar usuĆ”rio para ativo

  • Responder que tudo deu certo

Não utilizaremos banco de dados aqui, mas iremos criar uma função para simular. Para isso vamos atualizar nosso primeiro teste, aquele que esperamos sucesso e agora adicionaremos uma nova afirmação, onde um novo campo chamada is_inserted irÔ ser adicionado ao nosso map, apenas para exemplificar que foi inserido em nosso banco de dados que não existe. Vamos ao teste:

Se rodarmos esse teste, teremos um erro

Precisamos criar um novo comportamento onde simularĆ” salvar no banco de dados adicionando ao map a chave inserted_at: true.

Porém, temos um problema. Onde iremos colocar esse código? Teremos que tratar caso algum erro aconteça. Poderíamos utilizar o case e encadear mais case. Ficando algo assim:

Isso até pode funcionar, mas temos grandes perdas como ilegibilidade e este arquivo se tornarÔ gigante até finalizarmos todas as etapas que queremos, logo, a manutenção serÔ penosa. Podemos também usar pipes, estudamos sobre eles, porém, temos os problemas com os errors. Teríamos que tratar dentro de cada função o erro anterior e isso traria muitas indireções.

Para a nossa felicidade, temos uma declaração chamada with, que trabalha como um pipe, porem, cuidando de algumas coisas que ele não consegue fazer. Vamos usar ele e aprender como ele funciona.

Primeiro, um refactoring do que temos atƩ agora.

A resposta que esperamos da primeira função é {:ok, validated_user} caso não seja atendida a execução cairÔ no else e lÔ temos um tratamento simples de so passar o erro para frente.

Caso o erro de validação ocorra, ele cairÔ no else e no error tera o valor de {:error, "The field name is required"}. O legal da utilização do with é que qualquer coisa que não seja esperada como resultado nas função de composição, sera direcionadas para o else, onde podemos fazer tratamento ou so retornar seu erro de forma genérica. Logo isso ficarÔ mais claro.

Continuando. Precisamos agora salvar em nosso banco de dados falso e retornar o dado com a chave is_saved: true. As coisas se tornam mais simples daqui para frente. Precisamos criar uma função para salvar, chamarei de save/1 onde recebe o usuÔrio que precisamos salvar e adicionaremos na composição de funções do with.

Na função save/1 retorno apenas como sucesso, mas se você realmente quiser por isso em um banco, e queira retornar um {:error, reason} também irÔ funcionar, o with cuidarÔ do retorno do error.

Rodando isso, temos sucesso.

Mais uma etapa concluƭda, vamos agora ativar o usuƔrio:

  • Validar se os dados estĆ£o corretos;

  • Salvar no banco de dados

  • Atualizar usuĆ”rio para ativo

  • Responder que tudo deu certo

Começamos pelo teste, adicionado a afirmação em nosso teste de sucesso que esperamos agora um campo is_activated: true.

Vamos implementar esse comportamento. Precisamos de uma função semelhante ao save. Iremos criar a função activate/1 passando o usuÔrio que queremos ativar e então adicionaremos o campo e retornaremos o novo valor.

Perceba como ficou simples adicionar novas funƧƵes a funcionalidade. Ɖ fĆ”cil de ler, fĆ”cil dar manutenção e fĆ”cil remover. Era isso que estĆ”vamos procurando.

E por fim, a Ćŗltima etapa. Responder que tudo deu certo.

  • Validar se os dados estĆ£o corretos;

  • Salvar no banco de dados

  • Atualizar usuĆ”rio para ativo

  • Responder que tudo deu certo

Na verdade, jÔ fazemos isso. Quando utilizamos with, precisamos jÔ ter o que responder. Podemos transformar dados ou chamar outras funções, isso depende de cada cenÔrio. Mas sabemos que, ao entrar no bloco de execução do, todas as etapas anteriores foram feitas com sucesso e podemos ir sem medo para a próxima.

O código final de nossa implementação ficou assim:

Conclusão

A declaração with é poderosa, podemos usar para compor uma funcionalidade completa. Isso nos ajuda na legibilidade e manutenibilidade. Também nos força a pensar de forma funcional, uma vez que só podemos usar ela como função.

Existem mais sobre with que podemos aprender, mas acredito que esse capitulo ficou grande o suficiente.

Last updated