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
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
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":
Para testarmos, vamos rodar o comando mix credo
no terminal
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.
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
E o credo irá nos retornar:
Line is too long (max is 120, was 164).
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)
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:
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.
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:
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)