DEV Community

mateusmartins
mateusmartins

Posted on • Updated on

Aprendizado Elixir - módulo 2

Nesse módulo, criaremos um projeto chamado reports_generator, então já vamos cria-lo no terminal:

mix new reports_generator


* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/reports_generator.ex
* creating test
* creating test/test_helper.exs
* creating test/reports_generator_test.exs
Enter fullscreen mode Exit fullscreen mode

Desta vez, já iremos utilizar uma lib externa, então iremos aprender a instalar uma lib externa.

A lib em questão será o CREDO.

Para instala-lo, basta pesquisar por “elixir credo” no google, ou acessar o link.

Mas afinal, o que é o credo?

O credo é um analisador sintático de código. Ele Irá nos ajudar a manter boas práticas de escrita do nosso código, além do mix format que irá nos ajudar a formatar o código.

Para instalar o credo, basta acessar a pasta “mix .exs” dentro do projeto e ir até a função “defp deps do”.
Lá iremos inserir:

{:credo, "~> 1.6", only: [:dev, :test], runtime: false}

No terminal, utilizamos o comando “mix deps.get” para instalar as libs.

Agora, uma vez que o credo está instalado, rodaremos o comando:

mix credo gen.config
Enter fullscreen mode Exit fullscreen mode

Este comando irá gerar um arquivo de configuração do credo que podemos configura-lo para decidirmos o que queremos que ele cheque ou não.

Agora, se visualizarmos dentro do nosso código, veremos que foi criada uma pasta com o nome “.credo.exs":

Image description

Para testarmos, vamos rodar o comando mix credo no terminal

Image description

Como estamos em um projeto recém criado, ele não irá apresentar nenhuma issues.
Porém, se entrarmos dentro da lib “reports_generator.ex” e apagarmos a doc e rodarmos novamente o comando mix credo, ele irá nos devolver a seguinte mensagem:

3 mods/funs, found 1 code readability issue.
Enter fullscreen mode Exit fullscreen mode

Agora, ele nos diz que os módulos devem ter documentação.
Como não iremos trabalhar com documentação, iremos a pasta credo.exs e adicionar “false” em um dos nossos checks:

{Credo.Check.Readability.ModuleDoc, false},

Outra coisa que podemos fazer é observar que o credo vem como padrão um limite por linhas (nesse caso, 120)
Para obtermos esse resultado no credo, precisamos rodar um comando mais severo do credo:

 mix credo —strict  
Enter fullscreen mode Exit fullscreen mode

E o credo irá nos retornar:

Line is too long (max is 120, was 164).
Enter fullscreen mode Exit fullscreen mode

Existe uma extensão no vscode chamada Elixirlint que executa de forma dinâmica e executa os erros de forma ao vivo, sem precisarmos de rodar os checks o tempo todo.

Pra darmos continuidade ao nosso projeto, utilizaremos um arquivo que irá conter todos os CSV`s que iremos precisar.

Vamos alterar o nome da função para def build(filename) que irá receber o nome do arquivo que queremos abrir
Para ler o arquivo, colocamos dentro da nossa função:

File = File.read(“reports/#{filename})”)

Agora, vamos rodar o iex -S mix para tentar abrir um arquivo do nosso projeto


iex(1)> ReportsGenerator.build("report_test.csv")
{:ok,
"1,pizza,48\r\n2,açaí,45\r\n3,hambúrguer,31\r\n4,esfirra,42\r\n5,hambúrguer,49\r\n6,esfirra,18\r\n7,pizza,27\r\n8,esfirra,25\r\n9,churrasco,24\r\n10,churrasco,36"}

E obtemos um :ok.
Mas como podemos lidar melhor com o erro ao não encontrarmos um arquivo?
Na prática, criamos o “corpo” do case desta maneira:

Nós tentamos dar match em alguma ocasião , ou seja, se tivermos um “ok, result”, devolveremos result.
Mas, se recebermos um “error, reason”, devolveremos um reason (o motivo de error)

Image description

Agora, testamos no terminal as duas opções e ele irá nos retornar:


iex(4)> ReportsGenerator.build("report_test.csv")
"1,pizza,48\r\n2,açaí,45\r\n3,hambúrguer,31\r\n4,esfirra,42\r\n5,hambúrguer,49\r\n6,esfirra,18\r\n7,pizza,27\r\n8,esfirra,25\r\n9,churrasco,24\r\n10,churrasco,36"
iex(5)> ReportsGenerator.build("report_tawdest.csv")
:enoent

Uma outra forma, talvez mais elegante e mais aderente ao nosso exemplo quando entendermos o nosso módulo.

Vamos iniciar o conhecimento em Pipe Operator

Vamos dar um exemplo na prática para entendermos o seu funcionamento.
No terminal, vou criar uma String simples:

iex(4)> string = " aaaaaaaaAaaaAa \n"
" aaaaaaaaAaaaAa \n"

Agora, queremos gerar uma string sem o “/n” e sem esses “a” maiúsculos.
Uma opção seria passar: String.trim(string)
Assim, ele deletaria todos os espaços e o “/n”.
Só que estamos em uma linguagem imutável, então teríamos que atribuir a nossa string novamente:


String = String.trim(string)

E só assim modificamos a nossa string de fato.
Mas e se eu quiser deixa-la toda minúscula também?
Então teríamos que de fato fazer outra atribuição:


iex(10)> string = " aaaaAaaaaaa \n"
" aaaaAaaaaaa \n"
iex(11)> String.trim(string)
"aaaaAaaaaaa"
iex(12)> string = String.trim(string)
"aaaaAaaaaaa"
iex(13)> string
"aaaaAaaaaaa"
iex(14)> string = String.downcase(string)
"aaaaaaaaaaa"

Pouco legível e muita concatenação. Atrapalharia inclusive a leitura do código por outros desenvolvedores.

E onde o Pipe Operator entra?
Vamos seguir o mesmo exemplo de cima:


iex(16)> " aaaaaaAAAAAAaaaa \n" |> String.trim() |> String.downcase()
"aaaaaaaaaaaaaaaa"

Olha como fica legal, mais legível e extende bem menos o código.

O Pipe Operator nada mais faz do que pegar o primeiro o resultado de qualquer operação antes dele e passar pra função seguinte como primeiro argumento, e o resultado da da primeira função ele passaria pra segunda, e assim adiante, e por isso não há nada dentro dos parênteses, pois está vindo como resultado do “|>(pipe).

E como podemos aplicar isso dentro do nosso exemplo lá no projeto?

Vamos lá no código pegar o nome do nosso arquivo “filename” e concatenar com a variável reports:

Image description

Estamos pegando essa string: "reports/#{filename}" e passando para a primeira função |>File.read() e o resultado da função file.read eu estou passando para a função |>handle_file

Agora, vamos testar com um arquivo que existe:


iex(2)> ReportsGenerator.build("report_test.csv")
"1,pizza,48\r\n2,açaí,45\r\n3,hambúrguer,31\r\n4,esfirra,42\r\n5,hambúrguer,49\r\n6,esfirra,18\r\n7,pizza,27\r\n8,esfirra,25\r\n9,churrasco,24\r\n10,churrasco,36"

E quando tentamos um arquivo não existente, obtemos:


iex(3)> ReportsGenerator.build("report_tesawdt.csv")
"Error while opening file!"

Assim, conseguimos trabalhar com Pipe Operator e pattern matching, controlando o fluxo da nossa execução, sem dependermos de if&else ou “complicando” o nosso código.

Voltando ao nosso projeto, dentro temos um arquivo chamado “report_complete.csv” contendo 300 mil linhas.
Será com esse arquivo que iremos inicialmente trabalhar.

Em um cenário real, um arquivo contendo 1 milhão de linhas poderia ser muito pesado de ser carregado completamente na memória.
Então, a primeira coisa que iremos mudar é ao invés de utilizarmos “File.read()” nós iremos usar “File.stream!().

Usaremos um IO.inspect() para debugar no terminal o resultado desta operação. O IO sempre irá imprimir no terminal aquilo que passamos para ele como parâmetro.

Agora, faremos um recompile no terminal e iremos chamar novamente a função ReportsGenerator.build("report_test.csv”), e ele irá nos retornar:

`

%File.Stream{
line_or_bytes: :line,
modes: [:raw, :read_ahead, :binary],
path: "reports/report_test.csv",
raw: true
}
%File.Stream{
line_or_bytes: :line,
modes: [:raw, :read_ahead, :binary],
path: "reports/report_test.csv",
raw: true
}
`

Assim, ele nos traz uma Struct (nada mais é do que um map com um nome, resumidamente por hora)
O file.stream não traz o conteúdo em si, mas sim metadados

O file.stream possui um “!” no fim pois ele só abre arquivos existentes, não devolvendo tuplas com “error” ou “ok”.

Vamos usar o Enum.each e para cada elemento que tiver nesse arquivo (cada linha), daremos um IO.inspect

|> Enum.each(fn elem -> IO.inspect(elem) end)

E agora vamos chamar o mesmo comando iex(14)> ReportsGenerator.build("report_test.csv”)
E ele irá nos retornar:

`
%File.Stream{
line_or_bytes: :line,
modes: [:raw, :read_ahead, :binary],
path: "reports/report_test.csv",
raw: true
}
"1,pizza,48\n"
"2,açaí,45\n"
"3,hambúrguer,31\n"
"4,esfirra,42\n"
"5,hambúrguer,49\n"
"6,esfirra,18\n"
"7,pizza,27\n"
"8,esfirra,25\n"
"9,churrasco,24\n"
"10,churrasco,36"
:ok

`

Agora, pra cada linha do arquivo, ele leu e deu inspect.

Agora, faremos um exemplo utilizando Enum.map e para cada linha nós iremos trabalhar essa linha.
Por exemplo: nós iremos separar os elementos para visualizarmos de maneira melhor cada item.

Image description

Adicionamos algumas strings e mudamos o nosso enum e quando testamos novamente no terminal, ele nos retorna desta forma:


iex(22)> ReportsGenerator.build("report_test.csv")
[
["1", "pizza", "48"],
["2", "açaí", "45"],
["3", "hambúrguer", "31"],
["4", "esfirra", "42"],
["5", "hambúrguer", "49"],
["6", "esfirra", "18"],
["7", "pizza", "27"],
["8", "esfirra", "25"],
["9", "churrasco", "24"],
["10", "churrasco", "36"]
]

Mas por que utilizamos o &?
Quando criamos uma função anonima, podemos usar o fn com o E comercial e criar uma função de forma implícita.

Prosseguindo com o nosso parseamento do arquivo, podemos ver que ainda não está em um formato muito legal.
Estamos consumindo uma lista de listas basicamente, o que não seria muito legal.
Pra melhorarmos isso podemos utilizar a função reduce, que funciona da seguinte forma:

Enum.reduce([1,2,3,4], 0, fn elem, acc -> acc + elem end)

Nessa função, passamos a coleção de dados que queremos passar, no nosso caso uma lista “1, 2,3.4”, um valor de acumulador inicial, no nosso caso “0”, e uma função anonima “fn”, e essa função anonima sempre como primeiro elemento ela vai receber cada elemento da lista, e como segundo elemento o valor atual do acumulador, no nosso caso “acc"

Quando aplicamos a função dentro do nosso projeto, obtemos como resultado:

Image description

iex(4)> ReportsGenerator.build("report_test.csv")
%{
"1" => 48,
"10" => 36,
"2" => 45,
"3" => 31,
"4" => 42,
"5" => 49,
"6" => 18,
"7" => 27,
"8" => 25,
"9" => 24

Agora, ao invés de uma lista de listas, temos um Map onde temos o ID do usuário e quanto ele consumiu, de acordo com o nosso arquivo de teste.

Top comments (0)