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

  1. 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

  1. Escreva um módulo que defina uma função cria_usuario que receberá um nome, idade, email e a lista de endereço.
  2. 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:

  1. 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
    
  2. 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: #????
    
  3. 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:

  1. Adicione a biblioteca poison em sua última versão do Hex.pm. Baixe e compile!
  2. Veja o retorno desta função da lib Poison:
Poison.decode! "{ \"json_key\": \"json_value\" }"
  1. Faça um teste que chama uma url usando HTTPoison. Por exemplo:

https://api.bitbucket.org/2.0/repositories/suporte_concrete/desafio-android/pullrequests

  1. Implemente nossa função no módulo principal de forma que:
  2. Aceite o usuário e o repositório
  3. Retorne uma lista com mapas no seguinte formato:

%{ "usuario" => "XXXXXX", "repositorio" => "XXXXXXXXXXXXX" }

  1. Crie um teste que verifique que o resultado é uma lista que contém apenas mapas.
  2. Crie outra função que imprime no console a seguinte saída:

Repositório #{usuario}/#{repositorio}

Pull request: #{author} em #{created_on} …

Author: Victor Oliveira Nascimento victor.nascimento AT concretesolutions DOT com DOT br

Created: 2015-04-01 Qua 18:15

Validate