Gotas de Elixir – Manipulando listas

Manipular conjuntos de dados em Elixir é bem fácil utilizando os conceitos de Head e Tail junto com tail recursion e pattern matching!

Head e Tail

Ao receber uma lista como argumento de uma função, podemos dividi-la em duas partes: o primeiro elemento (Head) e o restante da lista (Tail). Veja no exemplo a seguir como é a sintaxe para receber um lista e dividi-la em head e tail:

defmodule PutsLista do
  def puts([ lista_head | lista_tail ]) do
    IO.puts("Head: #{lista_head}")
    IO.puts("Tail: #{lista_tail}")
  end
end
PutsLista.puts(["a", "b", "c"])
# mostra Head: a, Tail: bc

Dentro do corpo do método tanto a lista_head quanto lista_tail são utilizadas como variáveis. Lembre-se que lista_tail continua sendo uma lista, por tanto também possui um head e um tail.

Vamos modificar o código anterior para mostrar todos os elementos da lista um a um. Para isso vamos precisar criar duas funções diferentes, uma para processar uma lista normal e outra para um lista sem tail:

defmodule PutsLista do
  def puts([ lista_head | [] ]) do
    IO.puts("Head: #{lista_head}")
  end
  def puts([ lista_head | lista_tail ]) do
    IO.puts("Head: #{lista_head}")
    puts(lista_tail)
  end
end
PutsLista.puts(["a", "b", "c"])
# mostra Head: a, Head: b, Head: c

Utilizando o pattern matching, garantimos que a primeira definição de puts só será executada quando o tail da lista for uma lista vazia, ou seja quando a lista tiver apenas um elemento. Além disso, como a chamada recursiva ao puts é a última execução, garantimos também a otimização tail recursion.

O módulo Enum

Além da manipulação de listas com head e tail, Elixir também possui o módulo Enum que possui vários métodos úteis para lidar com listas. Assim você nem sempre precisará lidar diretamente com o head e tail.

O código anterior poderia ser reescrito utilizando Enum.each, que permite executar uma função (utilizamos a sintaxe para função anônima explicada nesse outro post) para cada um dos elementos da lista:

Enum.each(["a", "b", "c"], &(IO.puts("Head: #{&1}")))
# mostra Head: a, Head: b, Head: c

Outra função muito utilizada é a Enum.map, pois permite executar uma função em cada um dos elementos da lista mas ela coleta o retorno da função e cria um nova lista. Por exemplo:

Enum.map(["a", "b", "c"], &(String.upcase(&1)))
# Cria um nova lista ["A", "B", "C"]

Além de transformações também podemos filtrar elementos de uma lista com as funções Enum.filter (seleciona elementos de uma lista) e Enum.reject (rejeita elementos de uma lista). Ambas tomam a lista de elementos e uma função como argumento, mas o filter mantém elementos onde a função retorne true e o reject retira.

Enum.filter([1, 2, 3], &(Integer.is_even(&1)))
# Cria um nova lista [2]
Enum.reject([1, 2, 3], &(Integer.is_even(&1)))
# Cria um nova lista [1,3]

Além desses várias outras função do módulo Enum são úteis. Vale a pena conferir a documentação.

Confira mais conteúdo sobre Elixir!

Anúncios

Gotas de Elixir – 2 dicas para simplificar seu código

Aqui vão 2 dicas simples para deixar seu código Elixir mais idiomático e simples!

Para ilustrar vamos utilizar um exercício do Exercism.io onde precisamos escrever o módulo Acronimo que possui a função abreviar/1 (o /1 ao final do nome indica que a função toma um parâmetro como argumento). Esta função deve receber uma string como parâmetro e vai devolver apenas as primeiras letras em caixa alta, por exemplo Acronimo.abreviar(“Ruby on Rails”) retornaria “ROR”.

Este é um código que poderia resolver esse problema:

defmodule Acronimo do
  def abreviar(nome) do
    nomes_separados = String.split(nome)
    primeiras_letras = Enum.map(nomes_separados, fn(n) ->
      primeira_letra = String.first(n)
      String.capitalize(primeira_letra)
    end)
    Enum.join(primeiras_letras)
  end
end

Esse código funciona perfeitamente, mas não parece idiomático. Vamos refatorá-lo operadores que vão diminuir a quantidade de código e deixá-lo com mais cara de código Elixir 😉

1. O operador pipe |>

Escrever funções pequenas permite agrupá-las de maneira que nem precisamos criar uma variável que apenas guarda o valor de retorno e passa para a próxima função.

No código inicial criamos várias variáveis com esse propósito, mas utilizando o operador |> podemos encadear as chamadas e evitar a criação de variáveis.

defmodule Acronimo do
  def abreviar(nome) do
    String.split(nome)
    |> Enum.map(fn(n) ->
      String.first(n)
      |> String.capitalize
    end)
    |> Enum.join
  end
end

O operador pipe permite omitir o primeiro parâmetro do método, como por exemplo a chamada a Enum.map, que recebe a lista e a função de mapeamento. Outro exemplo mais simples é o código a seguir.

processar_pagamento(encontrar_funcionario(id), bonus_fixo)
# equivale a
encontrar_funcionario(id)
|> processar_pagamento(bonus_fixo)

2. Funções anônimas com o operador &

A segunda dica simplifica um pouco mais a chamada da função anônima dentro do map com o operador &, permitindo omitir a declaração do parâmetro, usando &1 em seu lugar, e diminuindo a quantidade de código.

defmodule Acronimo do
  def abreviar(nome) do
    String.split(nome)
    |> Enum.map(&(String.capitalize(String.first(&1))))
    |> Enum.join
  end
end

Utilizar uma função anônima tem suas vantagens caso o bloco de código dentro dela seja muito grande ou caso ela receba vários parâmetros. Mesmo podendo usar &1 e &2 para o primeiro e segundo parâmetros, o código pode não ficar tão legível.

O código final ficou bem mais conciso e parecido com código Elixir, e os dois operadores são bem simples de utilizar!

Se você curte Elixir, confira os posts da série Gotas de Elixir.

Gotas de Elixir – Imutabilidade

Programação Funcional não é nada novo e na verdade surgiu bem antes da Orientação a Objetos[1][2]. Mas hoje em dia elas vem se tornando bem famosas, especialmente pelo fato de que quase todo sistema utiliza-se de processamento paralelo.

Quando tínhamos uma capacidade computacional bem limitada, a programação funcional não fazia muito sentido, já que para evitar alterações no estado interno mais recurso é necessário. Mas esse tempo já passou e agora as linguagens funcionais estão voltando ao mundo comercial.

A principal vantagem de um programa desenvolvido em uma linguagem funcional é que ele não altera o seu estado interno. Assim, podemos escalar um programa funcional em vários cores paralelos, sem se preocupar com condição de corrida, deadlocks etc.

O que é o estado interno?

Veja as seguintes linhas de código em ruby:

total = 1900
operacao_maluca(total)
#podemos afirmar que o valor de total ainda é 1900?

Como não temos nenhuma garantia de o método operacao_maluca não altera seu argumento, não podemos ter certeza que o valor de total ainda é 1900. Por isso dizemos que o estado interno pode ser alterado. Se dois processos executarem a operaca_maluca ao mesmo tempo, fica impossível descobrir o que acontece com total.

Já esse mesmo pedaço de código escrito em Elixir, podemos ter certeza que o valor de total continuará sendo 1900, pois variáveis em elixir são imutáveis. Mas isso não quer dizer que total nunca poderá ser atualizado!

total = 1900
total = 1700
IO.puts total # 1900 ou 1700?

Explorando a Imutabilidade

Opa! Se variáveis são imutáveis, então total vai continuar sendo 1900 certo? Não! Como total é uma variável a referência para que ela aponta pode mudar! Desde que seja no contexto correto.

Observe o código a seguir e tente adivinhar o que é exibido na tela a cada execução de IO.puts:

total = 1900
defmodule Mutante do
  def mutar(valor) do
    valor = 1
    IO.puts valor # Aqui será exibido 1 ou 1900?
    valor
  end
end
Mutante.mutar(total)
IO.puts total # E aqui? 1 ou 1900?
total = Mutante.mutar(total)
IO.puts total # E agora, 1 ou 1900?

No primeiro IO.puts, dentro da função Mutante.mutar, será exibido 1! Temos uma variável definida como argumento e estamos alterando a sua referência. Antes ela apontava para uma parte da memória que continha 1900, agora aponta pra outra que contém 1.

Já no segundo IO.puts, depois da execução da função, deveria ser exibido 1, já que mudamos a referência dentro da função, certo? Não! Na verdade, fora da função, total continua apontado para 1900, a referência não pode ser alterada em outro lugar.

Para mudar o valor de total o que fazemos é mudar sua referência, como mostrado no último IO.puts, que agora sim mostrará total como 1. Então ser imutável não quer dizer que o valor nunca mudará, mas sim que eles está protegido de mudanças externas.

É possível ter Imutabilidade em linguagens não funcionais?

Claro que sim! Imutabilidade é uma decisão de design da aplicação, o que Elixir faz é forçar que isso nunca aconteça. Então mesmo que você use uma biblioteca desconhecida, terá certeza que valores não serão mudados inconscientemente.

A Imutabilidade é na verdade uma boa prática mesmo em linguagens Orientada a Objetos. Se o seu método altera um argumento, existem altas chances de surgirem bugs devido a esse efeito colateral. Sério, se você faz isso, pare agora. O seu eu do futuro vai agradecer.

O problema é que, por mais que você tome a decisão sensata de nunca mudar o estado de um argumento, não dá pra garantir que aquela gem que você usa em todos os projetos também segue essa ideia.

A maneira de garantir a imutabilidade é criando novas referências, o que leva ao ponto que comentei no começo do post sobre utilização de recursos. Numa época em que todo o processamento do mundo era menor que o do seu celular, isso fazia uma grande diferença. Mas agora isso não é mais um problema tão grande.

Gostou do post? Compartilhe com suas amigas e amigos! Se quiser saber mais sobre Elixir, veja os outros posts da série Gostas de Elixir!

Referências

[1] https://en.wikipedia.org/wiki/Functional_programming

[2] https://en.wikipedia.org/wiki/Object-oriented_programming

Gotas de Elixir – O que é tail recursion e como usar

Sabe quando você está aprendendo sobre chamadas recursivas e os professores e materiais avisam que nem sempre é uma boa ideia? Quando temos uma chamada recursiva, o compilador/interpretador precisa salvar o estado atual da chamada e criar uma nova chamada. Daí, ao executar a nova função a mesma coisa acontece, salva-se o estado anterior e faz-se uma nova chamada.

defmodule Fatorial do
  def normal(0), do: 1
  def normal(n), do: n * normal(n-1)
end

O código acima é a solução comum para o problema. Se você não entendeu o motivo de termos duas funções normal, veja o post anterior sobre pattern matching.

Apesar de simples, essa solução pode ser otimizada para evitar desperdícios de memória ao executar a recursão.

Tail Recursion

Tail Recursive é uma propriedade de funções em que não é necessário fazer nenhuma outra operação após a chamada recursiva a não ser retornar o valor. [1]

O exemplo anterior pode enganar pois a última ação da função é chamar ela mesma. Mas perceba que é preciso multiplicar n pelo retorno da função. Assim o compilador precisa ir salvando todos os valores de n para depois da última chamada recursiva calcular o valor de retorno.

Elixir implementa Tail Call Optimization, ou seja, se a função for otimizada para ao final apenas retornar o valor, sem nenhuma outra lógica, o compilador consegue economizar espaço em memória.

Podemos refatorar o código anterior e mover a multiplicação para dentro da chamada, com a ajuda de um acumulador e métodos auxiliares.

defmodule Fatorial do
  def otimizado(n), do: _fatorial_otimizado(n, 1)
  defp _fatorial_otimizado(0, acc), do: acc
  defp _fatorial_otimizado(n, acc), do: _fatorial_otimizado(n-1, acc*n)
end

Como vamos precisar de um acumulador, criamos a função privada _fatorial_otimizado para garantir que a interface do método continue a mesma (recebendo apenas o número n).

Inicialmente nosso acumulador será 1, para não interferir no cálculo do fatorial, já que n*1=n. Em seguida definimos o caso quando o número n chega a 0, retornando apenas o acumulador.

Por fim, lidamos com a recursão multiplicando n pelo acumulador antes de chamar a função com n-1. Assim, garantimos que depois da recursão não será necessário executar mais nada.

Nem tudo precisa ser otimizado

Premature optimization is the root of all evil – DonaldKnuth

(otimização prematura é a raiz de todo mal)

Elixir é uma linguagem bem eficiente com a ajuda da superpoderosa VM do Erlang. Esse exemplo do fatorial não consegui notar muita diferença entre o tempo de execução da versão normal e da otimizada.

A execução de Fatorial.normal(100_000) e Fatorial.otimizado(100_000) não teve ganhos significativos, mas quando o número subiu para 500.000 vi uma diferença de mais de um minuto. A diferença não está exatamente na velocidade de execução mas na memória consumida.

Para ter uma referência, implementando esse mesmo cenário em Scala, só consegui executar o fatorial de 100.000 depois que fiz a otimização para Tail Recursion. Antes disso a VM ficou sem memória.

Apesar de ser uma melhoria simples, é sempre bom entender melhor o contexto para tomar decisões de otimização.

 

Referências

[1] http://c2.com/cgi/wiki?TailRecursion

Gotas de Elixir – Pattern Matching em argumentos

Na série de posts Gotas de Elixir, vamos apresentar algumas características que tornam Elixir uma linguagem funcional bastante divertida de se aprender. Para mais informações sobre a linguagem visite a página oficial: elixir-lang.org

Na maioria das linguagens, o operador “=” é usado para atribuir valores para variáveis, em Elixir ele é utilizado para Pattern Matching.

Em uma olhada rápida, atribuir valores a uma variável é bem similar a outras linguagens, mas por baixo dos panos outras coisas acontecem.

a = 1

Fazendo o match de uma variável com o número 1, ligamos o valor para a variável. Podemos também tentar fazer o match de um número para uma variável que já tenha um valor atribuído:

a = 1
1 = a

Atenção, variáveis só podem ser atribuídas se estiverem no lado esquerdo do operador! Caso contrário, uma exceção será lançada pois o compilador vai entender que a “variável” é na verdade uma função:

1 = b
** (CompileError) iex:1: undefined function b/0

Caso não seja possível fazer o matching do lado direito com o lado esquerdo, uma exceção diferente será lançada:

a = 1
2 = a
** (MatchError) no match of right hand side value: 1

Agora vem a parte realmente legal, ao chamar uma função em Elixir o compilador tenta fazer o matching entre os valores recebidos e os parâmetros declarados na função. Assim, podemos evitar fazer comparações dentro do código e tratar casos diferentes em funções separadas.

O exemplo clássico é o cálculo do somatório da sequência de Fibonnaci. A ideia aqui é, dado um número inteiro e positivo, calcular a soma de todos os números seguindo a sequência de Fibonacci.

Em ruby, uma implementação poderia ser, dado um número n, validar se o número passado como parâmetro é 0 ou 1, retornando 0 e 1 respectivamente. Caso contrário, vamos somar o resultado da sequência de fibonacci de  n-1 e n-2:

def fibonacci(n)
  return n if n == 0 || n == 1
  fibonacci(n-1) + fibonacci(n-2)
end

Em elixir, podemos tirar vantagem do Pattern Matching em argumentos de funções e separar os comportamentos em funções diferentes. Para tratar os casos onde n é 0 ou 1, criamos as seguintes funções:

defmodule Fibonacci do
  def calcular_soma(0) do 0 end
  def calcular_soma(1) do 1 end
end

Em elixir podemos ter múltiplas definições de funções, com mesmo nome mas com regras de comparação de argumentos diferentes. Ao chamar a função Fibonacci.calcular_soma os valores passados como argumento serão verificados com as possíveis definições, assim não precisamos utilizar os ifs como na definição em ruby.

O último passo é implementar a função para outros valores de n. Basta adicionar uma nova definição da função que tem uma variável como parâmetro e em seguida seguir a lógica de somar a sequência de n-1 e n-2:

defmodule Fibonacci do
  def calcular_soma(0) do 0 end
  def calcular_soma(1) do 1 end
  def calcular_soma(n) do
    calcular_soma(n-1) + calcular_soma(n-2)
  end
end

Semelhante a ruby, funções retornam o valor da última linha, mesmo que não seja utilizado o “return”.

Outro ponto importante é notar que a ordem de declaração das funções dentro do módulo faz diferença. As definições específicas devem vir primeiro, pois ao executar Fibonacci.calcular_soma(1) o compilador deve encontrar primeiro a definição que recebe 1 como argumento, ao invés da genérica que recebe uma variável com valor qualquer.

O que aconteceria caso a ordem de definição das funções fosse a seguinte?

defmodule Fibonacci do
  def calcular_soma(n) do
    calcular_soma(n-1) + calcular_soma(n-2)
  end
  def calcular_soma(0) do 0 end
  def calcular_soma(1) do 1 end
end

Todas as chamadas à Fibonacci.calcular_soma iriam entrar em um loop infinito, pois ao tentar fazer o Pattern Matching com a primeiro definição da função, qualquer valor será compatível.

E ai, gostou de conhecer um pouco sobre Elixir? Nos próximos posts vamos mostrar como lidar com conjuntos pode ser bem fácil e divertido com Elixir!