DOJO sobre Elixir
Table of Contents
Guia para um DOJO de Elixir.
1 Começando
1.1 Instalando
Garanta que já está com o ambiente instalado. Instruções de instalação estão aqui.
1.2 Familiarizando-se com o shell
Para começar uma sessão do iex (Interactive Elixir Shell), faça o seguinte teste:
iex
Ao término da execução você deverá ver o shell esperando por comandos. Vamos começar experimentando pelo iex e logo na sequência iremos partir para a execução de scripts elixir.
Tentem o seguinte código e verifiquem a saída:
name = "Victor" IO.puts "Hello #{name}!"
Hello Victor!
1.3 Se virando no shell
Dentro de uma sessão de iex temos várias funções já implementadas. Veremos algumas a seguir.
1.3.1 Autocomplete
Por exemplo, temos autocomplete dos módulos do Elixir. Digitem "String." (sem as aspas) e apertem o tab.
O resultado será algo como:
iex(7)> String. Chars at/2 capitalize/1 chunk/2 codepoints/1 contains?/2 downcase/1 duplicate/2 ends_with?/2 first/1 graphemes/1 last/1 length/1 ljust/2 ljust/3 lstrip/1 lstrip/2 match?/2 next_codepoint/1 next_grapheme/1 printable?/1 replace/4 reverse/1 rjust/2 rjust/3 rstrip/1 rstrip/2 slice/2 slice/3 split/1 split/3 split_at/2 starts_with?/2 strip/1 strip/2 to_atom/1 to_char_list/1 to_existing_atom/1 to_float/1 to_integer/1 to_integer/2 upcase/1 valid?/1 valid_character?/1
1.3.2 Documentação
Existe uma função h que nos mostra a documentação de qualquer função. Experimentem o seguinte no shell: "h Enum.filter_map" (sem as aspas!!!).
O resultado será formatado! Algo como:
iex(8)> h Enum.filter_map def filter_map(collection, filter, mapper) Filters the collection and maps its values in one pass. Examples ┃ iex> Enum.filter_map([1, 2, 3], fn(x) -> rem(x, 2) == 0 end, &(&1 * 2)) ┃ [4]
1.3.3 Histórico de comandos
Ao chamar a função v (apenas digite v e enter), você poderá ver o histórico de sua sessão.
1.3.4 Outras utilidades
Existem milhares de outras funcionalidade que serão mais úteis ao decorrer do DOJO.
2 Tipos básicos da linguagem
Elixir e Erlang compartilham suas estruturas de dados primitivas. Vejamos a seguir quais são:
2.1 Tipos numéricos: integer e float
# Integers a = 1 a = 50 a = 100 # Na base hexadecimal a = 0x1F # Floats 1.0 # mesmo padrão que todas as linguagens :D
2.2 Constantes nomeadas: atom
# atom é uma constante nomeada a = :qualquer_valor b = :'qualquer valor com espaço' c = :'Qualquer valor maiúsculo com espaço' d = String # os nomes de módulos são atoms abreviados. Neste caso String é equivalente ao atom :'Elixir.String'. Veremos mais sobre módulos mais para frente # true e false são apenas um açúcar sintático para atoms :true e :false true = :true :false = false
2.3 String e lista de caracteres (exemplo e resultado)
# Uma string é inicializada com aspas duplas a = "uma string" # Há também docstrings (strings que mantém a formatação em múltiplas linhas) b = """ Estou testando uma docstring! Pulei linha e ainda estou dentro da docstring """ # Existe um outro tipo que é uma lista de caracteres que inicializado com aspas simples (usado mais em compatibilidade com o Erlang. Por hora é só importante saber que existe) c = 'abc' # Em Elixir podemos interpolar valores dentro de Strings. Para imprimirmos um tipo que não seja uma String podemos usar #{valor}. d = "#{123456}" IO.puts a IO.puts b IO.puts c IO.puts d
uma string Estou testando uma docstring! Pulei linha e ainda estou dentro da docstring abc 123456
2.4 Tipos de coleção: Listas, tuplas e mapas
# Uma lista é um conjunto de elementos variável. a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # Há uma sintaxe (tanto em Elixir e em Erlang) para tratar com listas # Para separarmos o primeiro item da lista do RESTO da lista usamos o | (pipe) [ primeiro_item | resto_da_lista ] = a # Ops... muito rápido? O que será que aconteceu? Não se preocupe com o sinal = (igual) estar estranho agora. Veremos isso a seguir. IO.puts "#{primeiro_item}" IO.puts "#{inspect resto_da_lista}" # Aqui usamos interpolação de String como vimos acima # Em contrapartida, tuplas são um conjunto de tamanho fixo e não possuem sintaxe especial b = {:ok, "isto é uma tupla taggeada"} IO.puts "#{inspect b}" # Usamos o inspect apenas ter uma representação em String do tipo. Veremos melhor adiante... # Mapas possuem uma sintaxe bem particular e muito legal de trabalhar. c = %{} # mapa vazio! d = %{"eu sou uma chave" => "eu sou um valor"} # Para atualizar um mapa podemos usar um atalho de sintaxe d = %{ d | "eu sou uma chave" => "valor atualizado"} # ATENÇÃO! Ele só irá atualizar uma chave que já exista! Se tentar: %{ "uma chave inexistente" => 1} irá lançar uma exceção # Também temos em Elixir um tipo chamado de Keyword list. É uma lista de tuplas chave e valor. e = [{:chave1, "teste"}] # -> Uma lista com uma tupla de duas posições {chave, valor}. [{:chave1, "teste"}] = [chave1: "teste"] # -> true! # Isso é mão na roda para expressar JSON por exemplo. O seguinte exemplo é: # Ex: {"valor": 32, "nome": "Nome completo" } é igual em estrutura a: f = [valor: 32, nome: "Nome completo"] # Essa sintaxe encurtada também existe para mapas: g = %{chave: "valor"} g = %{g | chave: "outro valor"} # tanto Keyword lists quanto mapas em que as chaves são atoms, podemos acessar os valores através da sintaxe: IO.puts "Chamando valor da lista por um atalho -> #{inspect f[:valor]}" IO.puts "Chamando valor do mapa por um atalho -> #{g[:chave]}" # Especialmente para mapas com chaves em atoms podemos usar um atalho melhor ainda: IO.puts "Chamando valor de um mapa por um atalho melhor ainda -> #{g.chave}" # O truque está em um conceito chamado Protocol do Elixir. Não veremos protocols em detalhes, mas fica a dica!
1 [2, 3, 4, 5, 6, 7, 8, 9, 10] {:ok, "isto é uma tupla taggeada"} Chamando valor da lista por um atalho -> 32 Chamando valor do mapa por um atalho -> outro valor Chamando valor de um mapa por um atalho melhor ainda -> outro valor
2.5 Só tem isso?
Existem outros tipos na linguagem que falaremos conforme formos avançando. Apenas para os curiosos temos ainda PIDs, structs e outros…
2.6 Exercícios
- Criem um mapa que represente um usuário que possua nome, idade, email, uma lista de endereços.
3 Arquivos de código fonte
Agora chega de só usar o shell e vamos criar arquivos com nossos códigos. Crie uma pasta elixir-dojo onde vamos colocar nosso código.
3.1 Primeiro arquivo
Crie um arquivo module.exs. Façamos um teste. Escrevam no arquivo:
IO.puts "Yo!" # projeto para ganhar milhões
Yo!
Para executarmos nosso arquivo podemos simplesmente executar no shell:
elixir module.exs
Isso irá avaliar todo o código que criamos dentro do arquivo. Reparem que não precisamos de classes, módulos, pacotes, namespaces e etc.
No entanto, vamos logo logo querer separar nosso código em unidades lógicas para ficar mais fácil reutilizar e referenciar.
Para isso usamos módulos. Eles são o equivalente ao escopo de classe em linguagens como Ruby, Java e etc. Nós já referenciamos módulos antes quando queríamos imprimir algo na tela:
IO.puts(String.capitalize("teste"))
Teste
Neste exemplo usamos o módulo IO e String da SDK do Elixir. Chamamos funções definidas DENTRO de cada módulo.
Antes de começarmos a usar muitas funções, vamos ver como podemos criar uma função.
Apaguem tudo no arquivo module.exs e digitem o seguinte:
defmodule PrimeiroModulo do # tudo dentro de do ... end estará DENTRO do módulo def diga_ola nome do IO.puts "Olá mundo! Meu nome é #{nome}" end end # Vamos chamar nosso modulo e funcao PrimeiroModulo.diga_ola "Elixir"
Olá mundo! Meu nome é Elixir
Usamos a macro "defmodule" para criar um módulo. Não iremos falar de macros agora, mas agora pense nela como uma palavra reservada.
Da mesma forma, definimos funções com "defun".
Atenção!
Parênteses em Elixir serão QUASE sempre opcionais. Sempre que o compilador conseguir desambiguizar eles serão opcionais. Há casos em que ele ficará na dúvida se a variável é parte de uma função ou é a chamada para outra função. Assim, caso tenham erros, verifiquem se o compilador está conseguindo desambiguizar.
3.2 Exercícios
- Escreva um módulo que defina uma função cria_usuario que receberá um nome, idade, email e a lista de endereço.
- Escreva outra função que crie um endereço com logradouro, número e cep.
4 Linguagem funcional
Até aqui tudo parece muito com Java, Python e, principalmente Ruby. Mas não esqueçam que estamos falando de uma linguagem funcional.
4.1 Pattern Matching
Vimos anteriormente que o sinal de igual se comporta um tanto quanto estranhamente… Vamos recapitular:
# Exemplo de lista [ primeiro_item | resto_da_lista ] = [1,2,3,4,5]
Isso chama-se "pattern matching". Ao invés de pensarmos no igual como uma atribuição (a variável passa a representar um valor), tratamos como uma expressão de igualdade.
O lado esquerdo do igual precisa ser igual ao lado direito. Assim, vejamos alguns exemplos:
a = 1 # a é uma variável sem valor, então o pattern matching atribui 1 ao valor para que a expressão seja verdadeira [a] = [1] # true b = [a] # true. b não tinha valor [c, d, e] = [ a, "testando", b] # true. O padrão irá atribuir as variáveis do lado direito ao lado esquerdo para a expressão ser verdadeira
Por isso chamamos o sinal de '=' de operador "match". Caso o lado esquerdo não seja igual ao lado direito ele lançará uma exceção.
4.2 Pattern Matching em funções
Podemos utilizar pattern matching para definir funções em Elixir. Vejam alguns exemplos:
defmodule Teste do def diga_ola "Elixir" do IO.puts "Olá Elixir!" end def diga_ola "Mundo" do IO.puts "Não..." end end # A plataforma irá chamar a primeira definição de diga_ola Teste.diga_ola "Elixir" # A plataforma irá chamar a segunda definição de diga_ola Teste.diga_ola "Mundo" defmodule TesteMaisComplexo do def calcula_area {:quadrado, {:lado, lado}} do lado * lado end def calcula_area {:retangulo, {:largura, largura}, {:altura, altura}} do largura * altura end end IO.puts "-------------" IO.puts "Quadrado: #{inspect TesteMaisComplexo.calcula_area {:quadrado, {:lado, 10}}}" IO.puts "Retangulo #{inspect TesteMaisComplexo.calcula_area {:retangulo, {:largura, 5}, {:altura, 10}}}"
Olá Elixir! Não... ------------- Quadrado: 100 Retangulo 50
Neste exemplo definimos uma função (e não duas!!!) que pode ter duas entradas diferentes: um quadrado ou um retangulo.
Quando chamamos a execução o Elixir irá fazer um "pattern matching" para saber qual implementação ele deve chamar.
Caso não encontre uma implementação, adivinha… exception :D. Assim é possível ter um caso genérico ao definir uma função.
defmodule PatternMatchingCasoGenerico do def eh_um_pinguim(:pinguim), do: true def eh_um_pinguim(_), do: false end IO.puts PatternMatchingCasoGenerico.eh_um_pinguim :pinguim IO.puts PatternMatchingCasoGenerico.eh_um_pinguim :ornitorrinco
true false
Para fazer um "match" genérico utilizamos o underscore '_'.
Atenção!
Usamos uma sintaxe encurtada para definir uma função. Quando o retorno cabe emum linha só, ao invés de escrevermos:
def nome() do … end
Podemos utilizar:
def nome(parametros), do: …
Atenção!
O uso de parênteses no Elixir será sempre opcional quando a plataforma conseguir detectar que não há ambiguidade.
Por exemplo:
def eh_um_pinguim :pinguim, do: true
Irá lançar uma exceção porque a plataforma não identifica se ", do" é outro parâmetro da função. Por isso utilizamos os parênteses.
Apenas mais um exemplo para mostrar o poder de pattern matching em funções com argumentos keyword.
defmodule KeyWordMatch do def busca_usuario name: name do IO.puts "Buscando usuário pelo nome #{name}" end def busca_usuario email: email do IO.puts "Buscando usuário pelo e-mail #{email}" end def busca_usuario idade: idade do IO.puts "Buscando usuário pela idade #{inspect idade}" end end KeyWordMatch.busca_usuario name: "Joseph Klimber" KeyWordMatch.busca_usuario email: "Joseph.Klimber@pesodepapel.com" KeyWordMatch.busca_usuario idade: 10
Buscando usuário pelo nome Joseph Klimber Buscando usuário pelo e-mail Joseph.Klimber@pesodepapel.com Buscando usuário pela idade 10
4.3 Funções anônimas
Elixir não seria uma linguagem funcional se não tivesse "fun"s, ou seja, funções anônimas :D Vejamos alguns exemplos:
# A assintatura de uma fun é: # # fn parametros -> corpo end a = fn nome -> IO.puts "Hello fun #{nome}" end # fn é uma palavra reservada # parametros pode ser nenhum, um ou muitos: b = fn -> IO.puts "Sem parâmetros" end c = fn param1, param2, param3 -> IO.puts "Muitos parâmetros #{param1} - #{param2} - #{param3}" end # para chamar uma função anônima em Elixir é só utilizar a sintaxe: a.("Elixir") b.() c.("1", "2", "3")
Hello fun Elixir Sem parâmetros Muitos parâmetros 1 - 2 - 3
4.4 Recursividade
Muita gente lembra com horror recursividade da faculdade… Provas "maledetas" com perguntas complicadas sobre cálculos recursivos não soa nada bacana…
No entanto, é uma parte fundamental do Elixir e veremos que não é nenhum bixo de sete cabeças. Prontos para o exemplo clássico?
defmodule Fibonacci do def fib(1), do: 1 def fib(2), do: 1 def fib(n) when is_integer(n) and n > 2 do fib(n - 1) + fib(n - 2) end def fib(_) do raise ArgumentError, message: "Número inválido" end end IO.puts Fibonacci.fib(5) IO.puts Fibonacci.fib(1) IO.puts Fibonacci.fib(2) IO.puts Fibonacci.fib(6)
5 1 1 8
Vamos por partes:
- Usamos a notação "encurtada" para definir nossas cláusulas
- Usamos o que chamamos de guard. Falaremos com mais detalhes a frente
- Usamos o '_' para o caso genérico. Qualquer valor que não seja um número e esteja de 1 para cima.
- Lançamos uma exceção :D. Veremos que isso não é uma boa prática de acordo com o OTP. Em geral os retornos são tuplas do tipo {:ok, valor} ou {:error, "Número inválido}
- Há um truque de sintaxe aqui… repare no argumento de raise: módulo e ???? Alguém? … É uma Keyword list. Como ela é o último parâmetro, o Elixir não exige os colchetes.
Mais alguns exemplos de funções recursivas:
defmodule FamosaRecursao do # funções públicas! def soma(lista) when is_list(lista), do: soma(lista, 0) def map(lista, funcao) when is_list(lista) and is_function(funcao) do map(lista, funcao, []) end # funções privadas # caso básico: quando a lista é vazia temos que parar a recursão defp soma([], acumulador), do: acumulador # recursão foderosa... defp soma [num | lista], acumulador do soma lista, acumulador + num end # exatamente a mesma coisa... defp map([], _funcao, acumulador), do: acumulador defp map([ item | lista], funcao, acumulador) do # usando funções anônimas como parâmetros map(lista, funcao, [ funcao.(item) | acumulador ]) end end IO.puts "#{inspect FamosaRecursao.soma [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}" IO.puts "#{inspect FamosaRecursao.soma Enum.to_list(1..100)}" IO.puts "#{inspect FamosaRecursao.map [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], fn numero -> numero + 1 end}" IO.puts "#{inspect FamosaRecursao.map Enum.to_list(1..100), fn numero -> numero + 1 end}"
55 5050 [11, 10, 9, 8, 7, 6, 5, 4, 3, 2] [101, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, ...]
4.5 Guards
Vimos alguns exemplos de definições de funções que tinham "guards", mas o que será isso?
A filosofia de um sistema tolerante a falhas é: "não escreverás código defensivo". Isso quer dizer: "deixa quebrar logo".
Até parece que não há uma preocupação com o fluxo do sistema, mas na verdade isso apenas evidenciará erros nos nossos sistemas antes no ciclo de desenvolvimento.
No entanto, podemos indicar melhor qual a intenção da função através de condições claras do que esperamos. Fazemos isso com "guards" que nada mais são do que funções especiais.
Normalmente estas funções estão no escopo do Kernel e retornam true ou false. Os que mais utilizaremos são as funções is_*alguma coisa*(parametro)
Exemplos:
defmodule GuardTest do def f1(param) when is_list(param), do: IO.puts "Sou uma lista" def f1(param) when is_binary(param), do: IO.puts "Sou um binário" def f1(param) when is_map(param), do: IO.puts "Sou um mapa" def f1(_), do: IO.puts "Sou qualquer outra coisa" end GuardTest.f1 "binário" GuardTest.f1 %{} GuardTest.f1 [] GuardTest.f1 :atom
4.6 Outros construtos do Elixir
Reparem que até agora sequer mostramos como funciona um "if" na linguagem nem outros controles básicos. Em verdade, é possível fazer qualquer coisa com o que temos na mão até o momento. É sério! Com pattern matching e recursão fica tudo muito mais fácil. Porém criar uma função para usar o pattern matching toda hora não é sempre conveniente. Por isso a linguagem possui outros construtos que também utilizam pattern matching:
# Primeiro e mais poderoso construto é o "case" defmodule Case do def case_test nome do # passamos qualquer variável para ele "testar" no pattern matching case nome do # CASO seja Elixir "Elixir" -> IO.puts "Seja bem vindo!" # CASO seja Java "Java" -> IO.puts "Cuidado com seu estado mutável!" # CASO seja qualquer outra coisa _ -> IO.puts "Qualquer outra coisa" end end end Case.case_test "Elixir" Case.case_test "Java" Case.case_test "Python" # também temos if, if/else, unless, unless/else a = if true, do: 1 b = if false do 3 else 4 end c = unless false, do: 5 d = unless true do 6 else 7 end IO.puts "a: #{a}\nb: #{b}\nc: #{c}\nd: #{d}"
Seja bem vindo! Cuidado com seu estado mutável! Qualquer outra coisa a: 1 b: 4 c: 5 d: 7
4.7 Exerícios
Criem um módulo que contenha as seguintes funções:
- Filter: dada uma lista de elementos retornar apenas aqueles que satisfazem a função de filtro. O resultado da função será sempre true ou false e não precisa se preocupar com guards.
def filter (lista_de_elementos, funcao_de_filtro) do ## ???? end
- Inversão de lista: lembra como o resultado sempre vem invertido da função map? Crie uma função que inverta o resultado.
def inverte(lista), do: #????
- Fatorial :D Dado um número positivo, calcular N * (N - 1 ) * (N - 2) … 1
def fac(numero), do: #????
5 Criando nosso primeiro projeto: MIX
Até agora estamos executando código em arquivos soltos sem uma estrutura de projeto bem definida. De agora em diante vamos criar projetos completos.
5.1 MIX: Elixir build tool
Equivalente a Maven, Gradle, NPM e um mix de outras ferramentas de Ruby e Python, MIX representa o sistema de build do Elixir. Vejamos as tasks do mix:
mix # Run the default task (current: mix run) mix archive # List all archives mix archive.build # Archive this project into a .ez file mix archive.install # Install an archive locally mix archive.uninstall # Uninstall archives mix clean # Delete generated application files mix cmd # Executes the given command mix compile # Compile source files mix compile.protocols # Consolidates all protocols in all paths mix deps # List dependencies and their status mix deps.clean # Remove the given dependencies' files mix deps.compile # Compile dependencies mix deps.get # Get all out of date dependencies mix deps.unlock # Unlock the given dependencies mix deps.update # Update the given dependencies mix do # Executes the tasks separated by comma mix escript.build # Builds an escript for the project mix help # Print help information for tasks mix hex.config # Read or update hex config mix hex.docs # Publish docs for package mix hex.info # Print hex information mix hex.key # Hex API key tasks mix hex.owner # Hex package ownership tasks mix hex.publish # Publish a new package version mix hex.search # Search for package names mix hex.user # Hex user tasks mix loadconfig # Loads and persists the given configuration mix local # List local tasks mix local.hex # Install hex locally mix local.rebar # Install rebar locally mix new # Create a new Elixir project mix run # Run the given file or expression mix test # Run a project's tests iex -S mix # Start IEx and run the default task
5.2 Primeiro projeto: mix new
Vamos criar um projeto desafio_cs. Primeiro vamos criar o projeto. Utilizemos o mix:
mix new desafio_cs
Os arquivos gerados são:
README.md # mix já cria um README do seu projeto .gitignore # também já ganha um .gitignore mix.exs # arquivo principal descritor da nossa aplicação config # diretório de configurações config/config.exs # configs default lib # diretório onde irá nosso código lib/desafio_cs.ex # arquivo principal do nosso projeto (modulo principal) test # diretório de testes test/test_helper.exs # arquivo para preparar o framework de test test/desafio_cs_test.exs # nosso primeiro arquivo de testes
Vemos logo de cara que um projeto em Elixir possui uma estrutura bem definida com tudo o que precisamos. Testes, configurações e nosso próprio código.
5.3 Arquivo mix.exs
Vamos ver o conteúdo padrão do arquivo.
defmodule DesafioCs.Mixfile do use Mix.Project def project do [app: :desafio_cs, version: "0.0.1", elixir: "~> 1.0", deps: deps] end # Configuration for the OTP application # # Type `mix help compile.app` for more information def application do [applications: [:logger]] end # Dependencies can be Hex packages: # # {:mydep, "~> 0.3.0"} # # Or git/path repositories: # # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} # # Type `mix help deps` for more examples and options defp deps do [] end end
Este é um módulo padrão do Elixir. Ele se equipara a um pom.xml ou build.gradle.
Talvez o mais diferente para olhos não treinados seja o valor da chave deps na keyword de retorno da função application. Ela nada mais é que a chamada da função deps sem argumentos.
Há uma palavra nova para nós que é o "use Mix.Project". Isto é uma forma de importarmos os módulo Mix.Project com um tempero a mais…
Não vamos tratar disso agora, mas a palavra "use" permite que o módulo referenciado injete funções no nosso módulo. Isso é um tópico avançado que envolve metaprogramação. Veremos isso mais adiante!
Os atributos do nosso projeto são pré definidos pela especificação do Mix. Na documentação há todas as possibilidades.
5.4 Testes e mais testes
Sei que ainda não falamos do que se trata nosso desafio, mas vejamos o que há no arquivo desafio_cs_test.exs
defmodule DesafioCsTest do use ExUnit.Case test "the truth" do assert 1 + 1 == 2 end end
Isso é um pouco irritante para um filósofo, mas vemos que há o teste "da verdade". De novo temos a palavra "use"…
Parece que esse código não compila né? Não existe a palavra reservada "test". Muito menos a assert. Porém, lembra que do que falamos sobre "use"?
Exato. ExUnit.Case injeta algumas funções no nosso módulo. É o caso das duas funções que falamos. A primeira recebe uma string (binary) e um bloco de execução. A segunda verifica se o operador é satisfeito dado o que é passado antes e depois dele.
Podemos executar este teste usando o mix. Na raiz do projeto é só digitar mix test. Para quem usa uma IDE ou plugin, verifique a documentação de cada um. Por exemplo, no Emacs o atalho é C-c a t.
Se não acontecer nada no reino da Matemática, o teste deve passar.
Para criarmos um novo teste basta criar outra chamada para "test". Exemplo:
defmodule DesafioCsTest do use ExUnit.Case test "the truth" do assert 1 + 1 == 2 end test "com string" do assert "minha string bonita" ~= "bonita" end test "que irá quebrar tudo" do assert "minha string bonita" ~= "sua feia" end end
Neste caso usamos outro operador: =~. Ele verifica se o RHS (right hand side), ou seja, o que há do lado direito dele está contido no lado esquerdo (LHS).
No primeiro caso "com string" tudo está ok. O RHS está contido no LHS. Porém no caso abaixo irá falhar. Veja que na mensagem de erro ele usa justamente RHS e LHS.
6 Desafio cliente de linha de comando do Bitbucket
Pronto! Podemos começar nosso desafio. Apenas para recapitular, nós vimos:
- Tipos básicos da linguagem
- Como criar funções em módulos e como chamá-las
- Como definir melhor o contrato das funções com guards
- Aspectos da linguagem funcional como recursão e pattern matching
- Como criar um projeto mix
- Como criar testes para o nosso projeto
- Como executar os testes do projeto
Agora vamos utlizar tudo isso para consultarmos uma API REST e exibí-la pelo console iex. Prontos?
6.1 Definindo nosso escopo e interface
Queremos fazer um request para um repositório e listar de forma legível todos os pullrequests associados àquele repositório. Simples não?
Primeiro vamos abrir nosso primeiro arquivo ".ex". Ele está dentro da pasta lib e por enquanto contém apenas:
defmodule DesafioCs do end
Nada demais, mas será nele que colocaremos nossas funções.
Agora, vamos pensar em como gostaríamos de chamar as funções. Os parâmetros parecem ser:
- usuário
- repositório
De tudo que vimos, podemos pensar em várias formas de definir esta função. Vejamos alguns exemplos:
defmodule DesafioCs do def pullrequests usuario, repositorio do :ok # faremos a busca aqui... end def pullrequests2 usuario: usuario, repositorio: repo do :ok # talvez um pouco mais idiomático... melhor? Não sei... end def pullrequests3 %{usuario: usuario, repositorio: repo} do :ok # mais feio... end end
Podemos pensar que a primeira opção e as demais são idênticas. Porém, se quisermos aumentar as opções no futuro teríamos que refatorar todos os lugares que faz esta chamada. Assim, vamos com a segunda opção que é bem simples e mostra a intenção da função.
Ok. Mas como eu faço uma requisição para uma API REST?
6.2 Dependências no MIX
Temos um cliente http puro em Erlang que poderíamos tranquilamente usar para fazer nossa chamada. No entanto, para facilitar nosso lado, utilizaremos uma dependência.
Para isso, basta lembrar que nosso arquivo mix.exs possui uma sessão de dependências. Mas existe um NPM para o Elixir? Um Maven Central? Oh yeah baby…
Já temos milhares de bibliotecas puras em Elixir no gerenciador de pacotes Hex. Desde banco de dados até calendário japonês.
Como encontrar uma lib de http? Basta acessar o portal e digitar http na busca.
Utilizaremos a lib :httpoison. Edite o arquivo mix.exs (de acordo com a documentação da lib):
def application do [applications: [:logger, :httpoison]] end defp deps do [{:httpoison, "~> 0.6"}] end
Para poder testar a lib, precisamos de mais alguns comandos do mix.
mix deps.get # espera baixar a lib (deve ser ridiculamente pequena) mix deps.compile # irá perguntar se deve instalar o rebar. Apenas aceite :D
Após compilar, vamos iniciar uma sessão iex com a dependência no load path:
iex -S mix
Agora estamos em um shell iex com nosso projeto no path mais nossas dependências. Este comando não é nada assim tão especial… com a flag -S podemos passar qualquer script Elixir. No caso passamos um script mix.exs. Ele sabe colocar as dependências no path e identificar que estamos em um projeto.
Agora vamos testar algumas coisas. Digite os seguintes comandos no shell:
# Vamos testar o HTTPoison! HTTPoison.get "https://api.bitbucket.org/2.0/repositories/suporte_concrete/desafio-android/pullrequests"
Atenção!
Reparem que logo após digitar o nome do módulo você ganha autocompletion com TAB :D
6.3 Structs
Vimos que o resultado da API é um mapa meio estranho. Ao invés de retornar: %{} ele retorna um %HTTPoison.Response{}. Mas o que é isso?
Trata-se de mais um tipo do Elixir: um struct. Não precisamos entrar em detalhes agora, mas ele é apenas um mapa especial do Elixir. Pode-se fazer um pattern matching normalmente nele quase como se fosse um mapa. Para criar um struct utilizamos a macro "defstruct" mais uma keyword list com os nomes dos campos mais valores default:
defmodule Usuario do defstruct nome: "nome default" end defmodule Test do user = %Usuario{} IO.puts "#{user.nome}" # mesma sintaxe de mapa user = %Usuario{ user | nome: "outro nome" } IO.puts "#{user.nome}" # no entanto, não podemos utilizar funções do módulo Map para acrescentar chaves nao_eh_um_usuario = Map.put user, :chave, "valor" IO.puts "Não é um usuário: #{inspect nao_eh_um_usuario}\nUser: #{inspect user}" usuario_mais_uma_vez = Map.delete nao_eh_um_usuario, :chave IO.puts "Usuário mais uma vez: #{inspect usuario_mais_uma_vez}" end
nome default outro nome Não é um usuário: %{__struct__: Usuario, chave: "valor", nome: "outro nome"} User: %Usuario{nome: "outro nome"} Usuário mais uma vez: %Usuario{nome: "outro nome"}
Atenção!
Reparem que não é possível chamar o struct logo abaixo da sua definição. Para isso criamos outro módulo para separar os contextos.
É importante reparar que um struct nada mais é um mapa com chaves em atoms e com uma chave específica :__struct__: MODULE (nome do módulo). Com isso podemos até mesmo criar um struct dinamicamente:
defmodule Usuario do defstruct nome: "nome default" end defmodule Test do user = %Usuario{} IO.puts "#{inspect user}" # criando o memso usuário com um mapa dinamicamente outro_user = %{__struct__: Usuario, nome: "Outro User"} IO.puts "#{inspect outro_user}" # qual será o resultado? end
%Usuario{nome: "nome default"} %Usuario{nome: "Outro User"}
6.4 Exercícios
Vamos testar o que aprendemos até aqui:
- Adicione a biblioteca poison em sua última versão do Hex.pm. Baixe e compile!
- Veja o retorno desta função da lib Poison:
Poison.decode! "{ \"json_key\": \"json_value\" }"
- Faça um teste que chama uma url usando HTTPoison. Por exemplo:
https://api.bitbucket.org/2.0/repositories/suporte_concrete/desafio-android/pullrequests
- Implemente nossa função no módulo principal de forma que:
- Aceite o usuário e o repositório
- Retorne uma lista com mapas no seguinte formato:
%{ "usuario" => "XXXXXX", "repositorio" => "XXXXXXXXXXXXX" }
- Crie um teste que verifique que o resultado é uma lista que contém apenas mapas.
- Crie outra função que imprime no console a seguinte saída:
Repositório #{usuario}/#{repositorio}
Pull request: #{author} em #{created_on} …