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
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.
Declaração
do
, onde abrimos escopo para quando tudo der certo, ele fará. Muito usado para definir a resposta de uma função onde owith
esta sendo composto.
Declaração
else
, para quando algo sair errado. Aqui podemos usar a regra dopattern matching
para conseguir a resposta do erro.
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.
Composição Quando falamos de composição aqui, estamos falando sobre juntar várias funções com o intuito de fazer uma funcionalidade completa. Por exemplo, salvar usuário ou renderizar arquivos, onde precisamos passar por validações e outras operações até chegar ao resultado estimado.
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.
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.
Refatoração
Processo de modificar um sistema de software para melhorar a estrutura interna do código sem alterar seu comportamento externo.
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 dadosAtualizar 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 dadosAtualizar usuário para ativoResponder 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