Skip to content

Latest commit

 

History

History
621 lines (433 loc) · 18.9 KB

chapter2.livemd

File metadata and controls

621 lines (433 loc) · 18.9 KB

Learn4Elixir - Capítulo 2

Funções em Elixir

Neste capítulo abordaremos funções em Elixir.

Run in Livebook

Funções nomeadas e módulos

Uma função recebe zero ou mais valores como entrada e retorna um valor como saída. As entradas e saídas podem ser de todos os tipos de Elixir (inclusive dos que não mostraremos aqui).

Elixir já vem com várias funções. Estas funções fazem parte de módulos. Por exemplo, o módulo 'String' contém a função 'upcase', com aridade 2 (portanto, nos referimos a ela como 'String.upcase/2'), que converte todos os caracteres em uma cadeia de caracteres para letras maiúsculas.

String.upcase("não grite, por favor!")

Você pode ter achado estranho a aplicação de String.upcase/2 acima receber apenas um argumento. Isto acontece pois Elixir permite que sejam definidas funções com argumentos default. Neste caso, o segundo argumento é o modo. Se você não passar nada, é o mesmo que chamar da seguinte forma:

String.upcase("não grite, por favor!", :default)

Mas eu posso escolher entre quatro valores: :default, :ascii, :greek, :turkic. Vamos testar dois deles e observar a diferença.

# observe o "ã"
String.upcase("não grite, por favor!", :ascii)
# observe o I acentuado
String.upcase("não grite, por favor!", :turkic)

Definindo suas próprias funções nomeadas e seus módulos

Você pode definir seu próprio módulo usando 'defmodule'. E suas funções usando 'def'. Veja a sintaxe nos exemplos abaixo:

defmodule Matematica do
  def soma(num1, num2) do
    num1 + num2
  end

  def subtracao(num1, num2) do
    num1 - num2
  end
end

Observe que o formato é:

defmodule <NOME_DO_MODULO> do
  def <NOME_DA_FUNCAO>(<LISTA_DE_ARGUMENTOS>) do
    <CORPO_DA_FUNCAO>
  end
end

Depois que você define o módulo e o avalia, você pode usar suas funções.

Matematica.soma(30, 40)
Matematica.subtracao(30, 56)

Vamos definir outro módulo:

Enum.sum([1.0, 3, 4])
Enum.count([1, 34, 56, 78, 1])
defmodule Turma do
  def media(lista_notas) do
    Enum.sum(lista_notas) / Enum.count(lista_notas)
  end

  def aprovado?(nota) when nota >= 6 do
    true
  end

  def aprovado?(_), do: false
end

No exemplo acima, usamos duas funções do módulo Enum: uma para somar todos os valores de uma lista de notas (Enum.sum/1) e outra para contar quantas notas tem na lista (Enum.count/1). A partir disto, calculamos a média de todas as notas da turma.

Em 'Turma.aprovado?/1' usamos a convenção de terminar o nome da função que sempre retorna um valor lógico com '?'.

Na linha 6 'def aprovado?(nota) when nota>=6 do', usamos uma guarda, que é introduzida com 'when' logo após a lista de parâmetros. Não temos tempo aqui de entrar em todos os detalhes de guardas. Neste caso, estamos dizendo que se a nota for maior ou igual 6, 'true' será retornado. Caso contrário (observe o casamento de padrões em ação na linha 9 com o uso do '_'), 'false' será retornado. Ainda na linha 9, observe que em vez de:

def <NOME_DA_FUNCAO> <GUARDA_OPCIONAL> do
  <RESULTADO>
end

podemos escrever

def <NOME_DA_FUNCAO> <GUARDA_OPCIONAL>, do: <RESULTADO>

Podemos ter várias cláusulas (ou seja, vários 'def's) para uma mesma função. A primeira que se aplicar, ou seja, cujo padrão casar será usada.

Turma.media([10, 4, 6, 7, 8])
Turma.aprovado?(6)
Turma.aprovado?(5)
Turma.aprovado?(7)

Funções sem nome (anônimas)

Agora que você já sabe o que são funções, vamos falar pra você de um tipo de função que pode ser muito útil em alguns casos: as funções sem nome (também chamadas de "funções anônimas").

Funções anônimas são funções que, como o próprio nome diz, não têm um nome. Elas são descritas apenas definindo qual sua entrada (se alguma) e os comandos que vão produzir sua saída.

No exemplo abaixo, definimos uma função que recebe um valor 'num' (que representa um número) e retorna este número adicionado de 1.

fn num -> num + 1 end

Quando você avalia o código acima, você percebe que Elixir te dá algo como:

#Function<42.125776118/1 in :erl_eval.expr/6>

Este valor, por si só, não tem muita utilidade para nós.

Como então podemos aplicar a função anônima que acabamos de definir a um valor, como 2? Normalmente não fazemos isso em Elixir, mas uma solução é atribuir a função a uma variável.

f = fn num -> num + 1 end

E só depois chamar a função:

f.(2)

Perceberam que, para chamar a função 'f', precisamos colocar um ponto antes dos parênteses? É uma característica de Ellxir (que na prática não é um problema).

Outra forma é fazer a chamada diretamente (também usando o ponto):

(fn num -> num + 1 end).(2)

Funções de ordem mais alta

O uso realmente interessante de funções anônimas é em funções de ordem mais alta (higher-order functions). Funções de ordem mais alta recebem uma ou mais funções como argumento, ou retornam uma função.

O módulo Enum

O módulo Enum inclui várias funções de ordem mais alta.

Por exemplo, a função 'Enum.map/2' recebe, no exemplo abaixo, como entrada uma lista '[10, 20, 3, 45]' e uma função anônima de aridade 1 que soma 12 ao valor que receber:

Enum.map([10, 20, 3, 45], fn x -> x + 12 end)

A função 'Enum.reduce/3' recebe como argumento uma lista, um valor inicial e uma função de aridade 2. No exemplo abaixo, a lista é '[1, 2,3]', o valor inicial é 0 e a função soma um elemento da lista ao acumulador, que passa a ter o valor desta soma. Deste modo, o resultado será '0+1+2+3'.

Enum.reduce([1, 2, 3], 0, fn elemento, acumulador -> elemento + acumulador end)

E como seria um exemplo de uma função que retorna outra função? No módulo abaixo nós definimos uma função 'HOF.incrementador/1', que recebe como entrada um número e retorna uma função de aridade 1 que incrementa um outro valor a este número.

defmodule HOF do
  def incrementador(numero) do
    fn valor -> valor + numero end
  end
end

Ou seja, se eu chamar desta forma:

HOF.incrementador(3)

Eu obtenho uma função que incrementa 3 ao valor que receber. Portanto, se eu passar 5 a esta função, o resultado será 8.

HOF.incrementador(3).(5)

É útil? Não lembro de nenhuma utilidade deste tipo de função em meu código. Mas pode ser útil em bibliotecas bem específicas.

O operador Pipe

O operador pipe (representado pelos dois símbolos '|>') é uma forma de chamar funções que permite que as chamadas sejam encadeadas de uma forma elegante e prática.

Por exemplo, em vez de fazer:

require Integer
Enum.all?(Enum.map([1, 2, 3, 4], fn x -> x * 2 end), &Integer.is_even/1)

Podemos escrever:

require Integer

[1, 2, 3, 4]
|> Enum.map(fn x -> x * 2 end)
|> Enum.all?(&Integer.is_even/1)

Ou seja, o pipe envia o resultado de uma expressão como o primeiro parâmetro para a próxima chamada a função.

Funções privadas em Módulos

Uma funcionalidade que pode ser útil é a definição de funções privadas em módulos.

Nós vimos anteriormente como definir uma função em um módulo:

defmodule Mat do
  def media(num1, num2) do
    (num1 + num2) / 2
  end
end

Vamos supor que eu queira ter uma função auxiliar, uma função que é usada apenas pelas outras funções do módulo. Este é o caso em que definimos uma função previada e para isso, em vez de 'def', usamos 'defp'.

defmodule Matem do
  def media(num1, num2) do
    (num1 + num2) / 2
  end

  def fatorial(num) when num > 0 do
    fatorial(num, 1)
  end

  def fatorial(_num) do
    :error
  end

  defp fatorial(0, fatorial_atual) do
    fatorial_atual
  end

  defp fatorial(num, fatorial_atual) when num > 0 do
    fatorial(num - 1, fatorial_atual * num)
  end
end
Matem.fatorial(1000)

Exercícios

Exercício 1

Você lembra do exercício em que você fez no primeiro livebook? Percebeu que nesse capítulo implementamos ela utilizando o módulo Enum?

defmodule Turma do
  def media(lista_notas) do
    Enum.sum(lista_notas) / Enum.count(lista_notas)
  end

  def aprovado?(nota) when nota >= 6 do
    true
  end

  def aprovado?(_), do: false
end

Elixir tem diversos módulos que possuem funções que podem nos auxiliar a simplificar o desenvolvimento de nosso código, como vimos acima no uso de Enum e no inicio desse livebook com String.

  • Leia essas documentações de forma rápida por cima e busque nelas informações quando for resolver esse exercício: Enum, String e Map.

E então resolva esses problemas:

  1. Retorne o tamanho da String "pneumoultramicroscopicossilicovulcanoconiótico".
  1. Retorne uma Lista que possui os mesmos items da lista [:a, :b, :c] porém com os itens na ordem contrária, ou seja [:c, :b, :a].
  1. Retorne um novo mapa que é a união desses dois mapas:
primeiro_mapa = %{ :um  =>  1, :dois => 2 }
segundo_mapa  = %{ :tres  =>  3, :quatro => 4 }
  1. Retorne uma lista de palavras da sentença:
# execute essa célula de código
sentenca = """
Como de hábito, Policarpo Quaresma, mais conhecido por Major Quaresma, bateu em casa às quatro e quinze da tarde. Havia mais de vinte anos que isso acontecia. Saindo do Arsenal de Guerra, onde era subsecretário, bongava pelas confeitarias algumas frutas, comprava um queijo, às vezes, e sempre o pão da padaria francesa.
"""
  1. Multiplique todos os itens da seguinte lista e retorne a soma de todos seus items sobrados.
# execute essa célula de código
lista_de_numeros = [4, 8, 12, 16, 20, 24, 28, 32, 36]

Exercício 2

O departamento de Matemática da Universidade Técnica Federal do Paraná entrou em contato com você. Eles querem usar Elixir para trabalhar com Álgebra Linear e para isso precisam que você implemente um módulo em Elixir para que eles possam usar para fazer operações com Matrizes que eles precisam. Para eles é importante:

  • Soma de Matrizes
  • Subtração de Matrizes
  • Multiplicação de Matrizes
  • Multiplicação de Matriz por Escalar (chamado de Produto Escalar)
  • Determinante da Matriz (para Matrizes até 3x3)

Para a soma, subtração e multiplicação de matrizes, as matrizes inseridas poderão ter quaisquer dimensões (verifique se a operação é factível antes de executá-la).

Uma matriz é representada por Listas dentro de Listas em Elixir.

matriz = [[0,0,1], [0,1,0], [1,0,0]]

Você pode utilizar qualquer recurso ou técnica, incluindo implementar funções auxiliares, features que ainda não vimos até aqui ou qualquer outra coisa que julgar necessário. Porém, com o que viu até aqui deve ser o suficiente para que possa implementar essa solução.

Abaixo, temos um "esqueleto" da implementação. Os professores esperam que essa "API" (o nome do módulo, das funções e suas assinaturas) sejam respeitadas, pois não vão investigar a implementação, mas esperam usar chamando-as com esses nomes e assinaturas.

defmodule AlgebraLinear do
  def soma_matrizes(matriz_a, matriz_b) do
    # A fazer
  end

  def subtrai_matrizes(matriz_a, matriz_b) do
    # A fazer
  end

  def multiplica_matrizes(matriz_a, matriz_b) do
    # A fazer
  end

  def escalar(matriz, escalar) do
    # A fazer
  end

  def determinante(matriz) do
    # A fazer
  end
end

Exercício 3

Você é um desenvolvedor web que trabalha em uma empresa que oferece serviços de streaming de música. Você está usando Elixir para criar uma API que permite aos usuários pesquisar, ouvir e avaliar as músicas disponíveis na plataforma. Você recebeu uma tarefa de implementar uma função que recebe uma lista de músicas e retorna uma lista de recomendações baseadas nas preferências do usuário.

Uma música é representada por um mapa com as seguintes chaves:

  • :titulo - uma string com o nome da música
  • :artista - uma string com o nome do artista
  • :genero - uma string com o gênero musical
  • :duracao - um inteiro com a duração da música em segundos
  • :avaliacao - um número de 0 a 5 com a avaliação média da música pelos usuários
musica = %{
            titulo: "Starway to Heaven",
            artista: "Led Zeppelin",
            genero: "Rock",
            duracao: 482,
            avaliacao: 4.9
          }

Uma preferência do usuário é representada por um mapa com as seguintes chaves:

  • :genero - uma string com o gênero musical preferido pelo usuário
  • :duracao_min - um inteiro com a duração mínima das músicas que o usuário gosta em segundos
  • :duracao_max - um inteiro com a duração máxima das músicas que o usuário gosta em segundos
  • :avaliacao_min - um número de 0 a 5 com a avaliação mínima das músicas que o usuário quer ouvir
preferencia = %{
                 genero: "Rock",
                 duracao_min: 200,
                 duracao_max: 400,
                 avaliacao_min: 4.5
                }

A função de recomendação deve receber uma lista de músicas e uma preferência do usuário e retornar uma lista de músicas que atendem aos critérios da preferência, ordenadas por avaliação em ordem decrescente. Se houver mais de uma música com a mesma avaliação, a ordem deve ser alfabética pelo título.

Você deve usar funções anônimas e as funções map, filter e reduce para implementar essa função. Você pode usar qualquer recurso ou técnica que julgar necessário, mas com o que viu até aqui deve ser o suficiente para que possa implementar essa solução.

defmodule Recomendacao do
  def recomenda(musicas, preferencia) do
    # A fazer
  end
end
# Define uma lista de músicas
musicas = [
  %{titulo: "Hotel California", artista: "Eagles", genero: "Rock", duracao: 391, avaliacao: 4.7},
  %{titulo: "Rolling in the Deep", artista: "Adele", genero: "Pop", duracao: 228, avaliacao: 4.5},
  %{titulo: "Bohemian Rhapsody", artista: "Queen", genero: "Rock", duracao: 355, avaliacao: 4.9},
  %{
    titulo: "Billie Jean",
    artista: "Michael Jackson",
    genero: "Pop",
    duracao: 292,
    avaliacao: 4.4
  },
  %{titulo: "Yesterday", artista: "The Beatles", genero: "Rock", duracao: 138, avaliacao: 4.6},
  %{titulo: "Shape of You", artista: "Ed Sheeran", genero: "Pop", duracao: 233, avaliacao: 4.1},
  %{
    titulo: "No Woman, No Cry",
    artista: "Bob Marley",
    genero: "Reggae",
    duracao: 245,
    avaliacao: 4.8
  },
  %{
    titulo: "Redemption Song",
    artista: "Bob Marley",
    genero: "Reggae",
    duracao: 179,
    avaliacao: 4.7
  }
]
# Define uma preferência do usuário
preferencia = %{genero: "Pop", duracao_min: 200, duracao_max: 250, avaliacao_min: 4.5}
# Chama a função de recomendação com a lista de músicas e a preferência do usuário
Recomendacao.recomenda(musicas, preferencia)

Exercicio 4

Você está participando de um emocionante concurso de escultura de círculos, onde artistas de todo o mundo exibem suas obras-primas circulares. Cada participante contribui com uma lista de raios de círculos usados em suas esculturas. O júri está ansioso para premiar as melhores esculturas, e você quer dar um toque de tecnologia à sua apresentação.

Você decide criar um programa em Elixir para ajudar a analisar e apresentar os dados dos raios dos círculos de forma a impressionar os juízes. Seu programa será responsável por calcular várias estatísticas importantes dos raios, o que pode destacar a singularidade de sua escultura.

  • Encontrar o raio mínimo e máximo para mostrar o quão variados são os tamanhos dos círculos em sua escultura.

  • Calcular o diâmetro correspondente aos raios mínimo e máximo para destacar a amplitude das dimensões dos círculos.

  • Calcular a área correspondente aos raios mínimo e máximo para ilustrar a diversidade de áreas cobertas pelos círculos.

  • Determinar a média dos raios para mostrar a proporção média dos tamanhos.

  • Saber quantos círculos diferentes você utilizou em sua escultura.

  • Apresentar a lista de raios em ordem crescente e decrescente para enfatizar o equilíbrio artístico.

  • Calcular a variação dos raios para demonstrar a diversidade na escolha de tamanhos.

  • Destacar a mediana dos raios para mostrar uma dimensão central dos círculos usados.

circulos = [2, 4, 3, 6, 20, 17, 5, 11, 9, 13, 2, 20]

Neste exercício, a documentação oficial do módulo Enum vai ser de grande ajuda. Sinta-se à vontade para explorar os recursos e funções oferecidos por este módulo para realizar as operações desejadas em sua análise de dados. Boa sorte em seu concurso de escultura de círculos!"

defmodule CalculoEsculturas do
  def raio_minimo() do
    # A fazer
  end

  def raio_maximo() do
    # A fazer
  end

  def diametro_minimo() do
    # A fazer
  end

  def diametro_maximo() do
    # A fazer
  end

  def area_minima() do
    # A fazer
  end

  def area_maxima() do
    # A fazer
  end

  def media_raios() do
    # A fazer
  end

  def numero_de_circulos() do
    # A fazer
  end

  def ordena_raios() do
    # A fazer
  end

  def ordena_raios_descendente() do
    # A fazer
  end

  def variacao_raios() do
    # A fazer
  end

  def mediana_raios() do
    # A fazer
  end
end