Dicas e truques Elixir lang

iagoeffting

Iago Effting

Posted on November 3, 2022

Dicas e truques Elixir lang

Última atualização: 01/10/2022
Elixir: 1.14

Uma rápida e importante introdução

Esse artigo está vivo! Vou revisita-lo no momento onde achar conveniente, seja quando precisar atualizar ou corrigir algo. Se algo mudar na linguagem onde possamos tirar um maior proveito, eu irei reescrever parte do artigo e não criarei um segundo, utilizarei sempre esse =D

Bora lá!

Dicas&Truques

  1. Funções anónimas e Pattern match
  2. Utilizando pipeline e with
  3. Guard clause

Pattern match e Funções anónimas

Quando seu código precisa se mover livremente, de acordo com o tipo e/ou valor de dados de entrada, a primeira coisa que brota em nossas cabeças são condicionais, certo? O que faz total sentido. Por exemplo:

# usando condicional case
users = [
  %{role: :admin},
  %{role: :customer},
  %{role: :member}
]

Enum.map(users, fn user -> 
  case user do
    %{role: :admin} -> :admin,
    %{role: :customer} -> :customer,
    _user -> :unknown
  end
end)
Enter fullscreen mode Exit fullscreen mode

Já utilizamos pattern match ali em cima na condição de cada case, mas podemos extrair um pouco mais dele quando utilizamos funções anónimas.

Temos um truque que deixa as coisas mais emocionante e conciso. Você não precisa do escopo da função definido, basta resolver tudo diretamente nos parâmetros dela:

# usando pattern match na função anónima
users = [
  %{role: :admin},
  %{role: :customer},
  %{role: :member}
]

Enum.map(users, fn
  %{role: :admin} -> :admin
  %{role: :customer} -> :customer
  _user -> :unknown           
 end)
Enter fullscreen mode Exit fullscreen mode

⚠ Lembrando ⚠: Isso só funciona com funções anónimas. Funções nomeadas precisam da definição de seu escopo.

Declaração With e legibilidade

Quando encontramos algo novo que gostamos, usamos em todos os lugares ou mostramos para todas as pessoas.

Pipes foram assim para mim. Achei eles tão bonitos que queria usar para tudo. Mas ai coisas assim aconteciam:

# usando pipes
def update(id, params) do
  id
  |> get_user()
  |> validate_user(params)
  |> update_user()
  |> handle_response()
end
Enter fullscreen mode Exit fullscreen mode

A primeira vista é bonito, passa rapidamente o propósito dos pipes e o que queremos atingir, mas nos trás algumas complicações.

  1. O dado que passa por todos os pipes devem ser o mesmo ou a próxima função do pipe deve saber qual é a resposta da anterior. Então Pablito sabe demais.
  2. O tratamento de erro vai para dentro da próxima função ou o pipe quebra. Caso get_user retorne um {:error, reason} quem irá tratar vai ser o validate_user o que é estranho.

Caso sintam algum desses problemas, o elixir consegue nos ajudar. Existe uma função chamada with que irá nos dar uma mão.

A mesma lógica, mas feita com with:

# usando with com tratamento de erros
def update(id, params) do
  with {:ok, user} <- get_user(id),
       {:ok, user_validate} <- validate_user(user, params),
       {:ok, user_updated} <- update_user(user_validated) do
    {:ok, user_updated} # <-- aqui está nossa resposta do with
  else
    {:error, :not_found} -> {:error, "User not found"}
    {:error, :unprocessable_entity} -> {:error, "Params are wrong"}
    {:error, :duplicated_user} -> {:error, "User need to be unique"} 
    error -> error
  end
end
Enter fullscreen mode Exit fullscreen mode

O código se torna maior, mas ganhamos algumas coisas:

  1. as funções subsequentes não precisam entender nada das anteriores e vice-versa.
  2. O tratamento de erro sai da função. Uma vez que a pattern match não se satisfazer pela definição do with como no exemplo with {:ok, user} <- get_user(id) o with vai parar e cair no escopo else onde podemos fazer tratamentos utilizando diretamente o pattern match.

Também podemos isolar os erros, onde o tratamento passa a ser feito por outro modulo. O phoenix faz isso com os controllers, chamando a estratégia de fallback_controller deixando o código limpo e claro.

Resultado:

# usando with simplificado
def update(id, params) do
  with {:ok, user} <- get_user(id),
       {:ok, user_validate} <- validate_user(user, params),
       {:ok, user_updated} <- update_user(user_validated) do
    {:ok, user_updated} # <-- aqui está nossa resposta do with
  end
end
Enter fullscreen mode Exit fullscreen mode

Obs.: Gosto de pipes, de verdade. Mas utilizo eles onde tenho um controle mais restrito, isso é um gosto meu. Não seria um problema para mim, por exemplo, utilizar o pipe dentro de user_validate onde eu tenho controle de tudo. Mas uma vez que eu pegue funções de módulos diferente, acabo tentando tentando desacoplar lógica de dentro das funções que não a pertencem. É uma escolha minha e que me ajudou até então. Existem outras formas, essa é apenas uma delas.

Guard clause

Eu particularmente gosto dessa. Vamos supor que precisamos que um dado dentro de um map e esse dado esteja bem dentro, uns 3 níveis. Com esse dado eu tenho que saber o que irei fazer em seguida. O fluxo se alteraria dependendo dele. Como podemos resolver isso? A forma mais clássica é utilizando cond.

Vamos a um exemplo.

# usando condicional cond
def func(data) do
  cond do
    is_integer(data.inside.far.away) -> IO.inspect("Integer: #{data.inside.far.away}")
    is_bitstring(data.inside.far.away) -> IO.inspect("String: #{data.inside.far.away}")
    _ -> IO.inspect("I don't know what hack is that: #{data.inside.far.away}")
  end
end
Enter fullscreen mode Exit fullscreen mode

Relativamente elegante, porém meio grande certo? Utiliza boas funções para fazer o desvio de fluxo. Porém, alguns problemas aparecem:

  1. Essa função vai crescer com a quantidade de N tratamentos que precisarmos (se fosse por aqui o SOLID iria ferir o open/close principle, mas falaremos disso talvez uma outra hora).
  2. Fazer dessa forma pode dificultar a criação de tests devido a dificuldade de gerar o cenário (ainda esta simples ali, mas pense nas infinitas possibilidades)
  3. Também teremos que nos certificar de que quando essa função mudar, todos os cenários podem mudar junto, mesmo não tendo uma relação.
  4. em alguma hora a leitura será prejudicada também. Facilmente vejo essa função crescer pra mais de 200 linhas and beyond.

Podemos resolver isso usando algo chamado guard clause diretamente na definição da função. E a leitura fica linda:

# utilizando guard clause e pattern match
def func(%{data: %{inside: value}}) when is_integer(value) do
  IO.inspect("Integer: #{value}")
end

def func(%{data: %{inside: value}}) when is_bitstring(value) do
  IO.inspect("String: #{value}")
end

def func(data) do
  IO.inspect("I don't know what the hack is that: #{value}")
end
Enter fullscreen mode Exit fullscreen mode

o interessante disso é:

  1. A leitura fica simples
  2. Podemos facilmente trocar de módulos cada um deles, caso a implementação cresça
  3. Mantemos um contrato claro do que precisamos para a função funcionar.

⚠ Lembrando ⚠: Guard clause so funcionam com tipos primitivos e não funcionam com funções de módules, como por exemplo:

# exemplos de utilização de guard clause
def display(a) when is_integer(a), do: # ... ✅
def display(a) when is_integer(a), do: # ... ✅
def display(a) when a > 10, do: # ... ✅

def display(a) when MyModule.check(a), do: # ... ❌
Enter fullscreen mode Exit fullscreen mode

Aqui esta uma lista do que ele permite.

Conclusão

Todas essas dicas devem ser pensadas e levado em consideração o cenário que será utilizado. O importante é facilitar a vida do programador ao realizar alguma manutenção (não so você, como todo o seu time e futuro membros). Se a escolha for consciente, você estará bem para ir, caso não, você vai se transformar em um desenvolvedor que deseja fazer tudo inline ou com menos código, e isso é muito sério, você mata a facilidade de entendimento e leitura de código por causa de ego. Não seja assim.

Caso você esteja consciente disso, divirta-se refatornando trechos de códigos difíceis de ler e veja se alguma dessas dicas lhe foi útil, caso tenha sido, conte para nós nos comentários.


Já que você chegou até aqui, se inscreva no Café com Elixir uma newsletter semanal de Elixir focado em conteúdos da comunidade brasileira.

Me sigam também no twitter @iagoEffting lá sou mais ativo =D

E se inscrevam no canal no youtube

💖 💪 🙅 🚩
iagoeffting
Iago Effting

Posted on November 3, 2022

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

Sign up to receive the latest update from our blog.

Related

Dicas e truques Elixir lang
elixir Dicas e truques Elixir lang

November 3, 2022