Funções
Em Elixir e em várias linguagens funcionais, funções são cidadãos de primeira classe. Nós aprenderemos sobre os tipos de funções em Elixir, qual a diferença, e como utilizá-las.
Funções anônimas
Tal como o nome indica, uma função anônima não tem nome. Como vimos na lição Enum
, elas são frequentemente passadas para outras funções. Para definir uma função anônima em Elixir nós precisamos das palavras-chave fn
e end
. Dentro destes, podemos definir qualquer número de parâmetros e corpos separados por ->
.
Vejamos um exemplo básico:
iex> sum = fn (a, b) -> a + b end
iex> sum.(2, 3)
5
A & taquigrafia
Utilizar funções anônimas é uma prática comum em Elixir, há uma taquigrafia para fazê-lo:
iex> sum = &(&1 + &2)
iex> sum.(2, 3)
5
Como você provavelmente já adivinhou, na versão abreviada nossos parâmetros estão disponíveis como &1
, &2
, &3
, e assim por diante.
Pattern matching
Pattern matching não é limitado a apenas variáveis em Elixir, isto pode ser aplicado a assinaturas de funções como veremos nesta seção.
Elixir utiliza pattern matching para verificar todas as possíveis opções de match e identificar o primeiro conjunto de parâmetros associados para executar seu respectivo corpo.
iex> handle_result = fn
...> {:ok, result} -> IO.puts "Handling result..."
...> {:ok, _} -> IO.puts "This would be never run as previous will be matched beforehand."
...> {:error} -> IO.puts "An error has occurred!"
...> end
iex> some_result = 1
1
iex> handle_result.({:ok, some_result})
Handling result...
:ok
iex> handle_result.({:error})
An error has occurred!
Funções nomeadas
Nós podemos definir funções com nomes para referir a elas no futuro, estas funções nomeadas são definidas com a palavra-chave def
dentro de um módulo. Nós iremos aprender mais sobre Módulos nas próximas lições, por agora nós iremos focar apenas nas funções nomeadas.
Funções definidas dentro de um módulo são disponíveis para uso de outros módulos, isso é particularmente útil na construção de blocos em Elixir:
defmodule Greeter do
def hello(name) do
"Hello, " <> name
end
end
iex> Greeter.hello("Sean")
"Hello, Sean"
Se o corpo da nossa função apenas tem uma linha, nós podemos reduzi-lo ainda mais com do:
:
defmodule Greeter do
def hello(name), do: "Hello, " <> name
end
Armado com nosso conhecimento sobre pattern matching, vamos explorar recursão usando funções nomeadas:
defmodule Length do
def of([]), do: 0
def of([_ | tail]), do: 1 + of(tail)
end
iex> Length.of []
0
iex> Length.of [1, 2, 3]
3
Nomear Funções e a Aridade
Mencionamos anteriormente que as funções são nomeadas pela combinação do nome e aridade (quantidade dos argumentos) das funções. Isto significa que você pode fazer coisas como essa:
defmodule Greeter2 do
def hello(), do: "Hello, anonymous person!" # hello/0
def hello(name), do: "Hello, " <> name # hello/1
def hello(name1, name2), do: "Hello, #{name1} and #{name2}"
# hello/2
end
iex> Greeter2.hello()
"Hello, anonymous person!"
iex> Greeter2.hello("Fred")
"Hello, Fred"
iex> Greeter2.hello("Fred", "Jane")
"Hello, Fred and Jane"
Nós listamos os nomes das funções nos comentários acima. A primeira implementação não recebe argumentos, é conhecida como hello/0
; a segunda função recebe um argumento, portanto será conhecido como hello/1
e assim por diante. E, ao contrário de sobrecargar funções como em outras linguagens de programação, estas são pensadas como funções diferentes entre uma e outra. (Pattern matching, ou combinação de padrões, descrita agora há pouco, apenas se aplica quando várias definições são fornecidas com a mesma quantidade de argumentos.)
Funções e pattern matching
Nos bastidores, as funções fazem pattern match nos argumentos com as quais são chamadas.
Se precisássemos de uma função que aceitasse um mapa, mas estivéssemos interessados em utilizar apenas uma chave, poderíamos fazer o pattern match da presença da chave assim:
defmodule Greeter1 do
def hello(%{name: person_name}) do
IO.puts "Hello, " <> person_name
end
end
Agora digamos que temos um mapa descrevendo uma pessoa chamada Fred:
iex> fred = %{
...> name: "Fred",
...> age: "95",
...> favorite_color: "Taupe"
...> }
Estes são os resultados que teremos quando chamarmos Greeter1.hello/1
com o mapa fred
:
# call with entire map
...> Greeter1.hello(fred)
"Hello, Fred"
E o que acontece se chamarmos a função com um mapa que não contenha a chave :name
?
# call without the key we need returns an error
...> Greeter1.hello(%{age: "95", favorite_color: "Taupe"})
** (FunctionClauseError) no function clause matching in Greeter1.hello/1
The following arguments were given to Greeter1.hello/1:
# 1
%{age: "95", favorite_color: "Taupe"}
iex:12: Greeter1.hello/1
A razão para este comportamento é que o Elixir faz o pattern match entre os argumentos da chamada e a aridade da definição da função.
Vamos pensar sobre como estão os dados quando eles chegam a Greeter1.hello/1
:
# incoming map
iex> fred = %{
...> name: "Fred",
...> age: "95",
...> favorite_color: "Taupe"
...> }
Greeter1.hello/1
espera como argumento:
%{name: person_name}
Em Greeter1.hello/1
, o mapa que passamos (fred
) é comparado com nosso argumento (%{name: person_name}
):
%{name: person_name} = %{name: "Fred", age: "95", favorite_color: "Taupe"}
Ele encontra a chave que corresponde a name
no mapa de entrada. Temos um match! E como resultado desse match, o valor da chave :name
do mapa da direita (fred
) é atribuído à variável à esquerda (person_name
).
Agora, e se quiséssemos atribuir o nome de Fred a person_name
, mas TAMBÉM quiséssemos salvar todo o mapa? Digamos que queremos IO.inspect(fred)
após cumprimentá-lo. Como, até agora, fizemos pattern match apenas a chave :name
de nosso mapa, a função não tem conhecimento do resto de Fred.
De maneira a mantê-lo, precisamos atribuir o mapa inteiro a sua própria variável para que consigamos utilizá-lo.
Vamos criar uma nova função:
defmodule Greeter2 do
def hello(%{name: person_name} = person) do
IO.puts "Hello, " <> person_name
IO.inspect person
end
end
Lembre-se que Elixir vai fazer o pattern match no argumento da maneira que ele vir. Então, nesse caso, cada lado vai fazer pattern match com o argumento e atribuir a qualquer coisa que dê match. Vamos ver o lado direito primeiro:
person = %{name: "Fred", age: "95", favorite_color: "Taupe"}
Agora, person
foi avaliado e atribuído ao fred-map. Vamos ao próximo pattern match:
%{name: person_name} = %{name: "Fred", age: "95", favorite_color: "Taupe"}
Esse é o mesmo que nossa função original Greeter1
onde fizemos o pattern match e retemos apenas o nome de Fred. O que conseguimos foi que agora podemos usar duas variáveis em vez de uma:
-
person
, se referindo a%{name: "Fred", age: "95", favorite_color: "Taupe"}
-
person_name
, se referindo a"Fred"
Agora, quando chamarmos Greeter2.hello/1
, podemos utilizar toda a informação de Fred:
# call with entire person
...> Greeter2.hello(fred)
"Hello, Fred"
%{age: "95", favorite_color: "Taupe", name: "Fred"}
# call with only the name key
...> Greeter2.hello(%{name: "Fred"})
"Hello, Fred"
%{name: "Fred"}
# call without the name key
...> Greeter2.hello(%{age: "95", favorite_color: "Taupe"})
** (FunctionClauseError) no function clause matching in Greeter2.hello/1
The following arguments were given to Greeter2.hello/1:
# 1
%{age: "95", favorite_color: "Taupe"}
iex:15: Greeter2.hello/1
Vimos, então, que o Elixir faz um pattern match de múltipla profundidade, pois cada argumento faz match com a entrada de maneira independente.
Se alterarmos a ordem de %{name: person_name}
e person
na lista, teremos os mesmos resultados, pois cada um faz o pattern match com fred
de maneira separada.
Trocamos a variável e o mapa:
defmodule Greeter3 do
def hello(person = %{name: person_name}) do
IO.puts "Hello, " <> person_name
IO.inspect person
end
end
E chamamos com os mesmos dados que em Greeter2.hello/1
:
# call with same old Fred
...> Greeter3.hello(fred)
"Hello, Fred"
%{age: "95", favorite_color: "Taupe", name: "Fred"}
Lembre-se que, embora pareça que %{name: person_name} = person
está fazendo pattern match entre %{name: person_name}
e person
, na verdade eles estão ambos fazendo pattern match com o argumento de entrada.
Resumo: Funções fazem pattern match com os dados de entrada a seus argumentos de maneira independente. Podemos usar isso para atribuir valores a variáveis separadas dentro da função.
Funções privadas
Quando não quisermos que outros módulos acessem uma função específica, nós podemos torná-la uma função privada, que só podem ser chamadas dentro de seus módulos. Nós podemos defini-las em Elixir com defp
:
defmodule Greeter do
def hello(name), do: phrase <> name
defp phrase, do: "Hello, "
end
iex> Greeter.hello("Sean")
"Hello, Sean"
iex> Greeter.phrase
** (UndefinedFunctionError) function Greeter.phrase/0 is undefined or private
Greeter.phrase()
Guards
Nós cobrimos brevemente guards nas lições de Estruturas Condicionais, agora veremos como podemos aplicá-los em funções nomeadas. Uma vez que Elixir tem correspondência em uma função, qualquer guard existente irá ser testado.
No exemplo a seguir nós temos duas funções com a mesma assinatura, contamos com guards para determinar qual usar com base no tipo do argumento:
defmodule Greeter do
def hello(names) when is_list(names) do
names = Enum.join(names, ", ")
hello(names)
end
def hello(name) when is_binary(name) do
phrase() <> name
end
defp phrase, do: "Hello, "
end
iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"
Argumentos padrões
Se nós quisermos um valor padrão para um argumento, nós usamos a sintaxe argumento \\ valor
:
defmodule Greeter do
def hello(name, language_code \\ "en") do
phrase(language_code) <> name
end
defp phrase("en"), do: "Hello, "
defp phrase("pt"), do: "Olá, "
end
iex> Greeter.hello("Sean", "en")
"Hello, Sean"
iex> Greeter.hello("Sean")
"Hello, Sean"
iex> Greeter.hello("Sean", "pt")
"Olá, Sean"
Quando combinamos nosso exemplo de guard com argumento padrão, nos deparamos com um problema. Vamos ver o que pode parecer:
defmodule Greeter do
def hello(names, language_code \\ "en") when is_list(names) do
names = Enum.join(names, ", ")
hello(names, language_code)
end
def hello(name, language_code \\ "en") when is_binary(name) do
phrase(language_code) <> name
end
defp phrase("en"), do: "Hello, "
defp phrase("pt"), do: "Olá, "
end
** (CompileError) iex:8: def hello/2 defines defaults multiple times. Elixir allows defaults to be declared once per definition.
Instead of:
def foo(:first_clause, b \\ :default) do ... end
def foo(:second_clause, b \\ :default) do ... end
one should write:
def foo(a, b \\ :default)
def foo(:first_clause, b) do ... end
def foo(:second_clause, b) do ... end
Elixir não gosta de argumentos padrões em múltiplas funções, pode ser confuso. Para lidar com isto, adicionamos funções com nosso argumento padrão:
defmodule Greeter do
def hello(names, language_code \\ "en")
def hello(names, language_code) when is_list(names) do
names = Enum.join(names, ", ")
hello(names, language_code)
end
def hello(name, language_code) when is_binary(name) do
phrase(language_code) <> name
end
defp phrase("en"), do: "Hello, "
defp phrase("pt"), do: "Olá, "
end
iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"
iex> Greeter.hello ["Sean", "Steve"], "pt"
"Olá, Sean, Steve"
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!