Por que existem dois tipos de funções no Elixir?


279

Estou aprendendo o Elixir e me pergunto por que ele tem dois tipos de definições de função:

  • funções definidas em um módulo com def, chamado usingmyfunction(param1, param2)
  • funções anônimas definidas com fn, chamadas usandomyfn.(param1, param2)

Somente o segundo tipo de função parece ser um objeto de primeira classe e pode ser passado como parâmetro para outras funções. Uma função definida em um módulo precisa ser agrupada em a fn. Existe um pouco de açúcar sintático que parece otherfunction(myfunction(&1, &2))facilitar isso, mas por que é necessário em primeiro lugar? Por que não podemos simplesmente fazer otherfunction(myfunction))? É apenas para permitir funções de módulo de chamada sem parênteses, como no Ruby? Parece ter herdado essa característica de Erlang, que também possui funções e divertimentos de módulo. Então, na verdade, é como o Erlang VM funciona internamente?

Existe algum benefício em ter dois tipos de funções e converter de um tipo para outro para passar para outras funções? Existe um benefício em ter duas notações diferentes para chamar funções?

Respostas:


386

Apenas para esclarecer o nome, ambas são funções. Uma é uma função nomeada e a outra é anônima. Mas você está certo, eles funcionam de maneira um pouco diferente e vou ilustrar por que eles funcionam assim.

Vamos começar com o segundo fn. fné um fechamento, semelhante ao lambdado Ruby. Podemos criá-lo da seguinte maneira:

x = 1
fun = fn y -> x + y end
fun.(2) #=> 3

Uma função também pode ter várias cláusulas:

x = 1
fun = fn
  y when y < 0 -> x - y
  y -> x + y
end
fun.(2) #=> 3
fun.(-2) #=> 3

Agora, vamos tentar algo diferente. Vamos tentar definir cláusulas diferentes, esperando um número diferente de argumentos:

fn
  x, y -> x + y
  x -> x
end
** (SyntaxError) cannot mix clauses with different arities in function definition

Ah não! Temos um erro! Não podemos misturar cláusulas que esperam um número diferente de argumentos. Uma função sempre tem uma aridade fixa.

Agora, vamos falar sobre as funções nomeadas:

def hello(x, y) do
  x + y
end

Como esperado, eles têm um nome e também podem receber alguns argumentos. No entanto, eles não são encerramentos:

x = 1
def hello(y) do
  x + y
end

Esse código não será compilado porque toda vez que você vê um def, você obtém um escopo de variável vazio. Essa é uma diferença importante entre eles. Eu particularmente gosto do fato de que cada função nomeada começa com uma lista limpa e você não mescla as variáveis ​​de diferentes escopos. Você tem um limite claro.

Poderíamos recuperar a função hello nomeada acima como uma função anônima. Você mesmo mencionou:

other_function(&hello(&1))

E então você perguntou: por que não posso simplesmente passar isso hellocomo em outros idiomas? Isso ocorre porque as funções no Elixir são identificadas por nome e aridade. Portanto, uma função que espera dois argumentos é uma função diferente daquela que espera três, mesmo se eles tivessem o mesmo nome. Portanto, se simplesmente passássemos hello, não teríamos ideia do que hellovocê realmente quis dizer. Aquele com dois, três ou quatro argumentos? Esta é exatamente a mesma razão pela qual não podemos criar uma função anônima com cláusulas com diferentes áreas.

Desde o Elixir v0.10.1, temos uma sintaxe para capturar funções nomeadas:

&hello/1

Isso capturará a função nomeada local hello com arity 1. Em todo o idioma e sua documentação, é muito comum identificar funções nessa hello/1sintaxe.

É também por isso que o Elixir usa um ponto para chamar funções anônimas. Como você não pode simplesmente transmitir hellocomo uma função, é necessário capturá-la explicitamente, existe uma distinção natural entre funções nomeadas e anônimas e uma sintaxe distinta para chamar cada uma delas torna tudo um pouco mais explícito (Lispers estaria familiarizado com isso devido à discussão Lisp 1 vs. Lisp 2).

No geral, essas são as razões pelas quais temos duas funções e por que elas se comportam de maneira diferente.


30
Também estou aprendendo o Elixir, e esse é o primeiro problema que encontrei que me deu uma pausa, algo sobre isso parecia inconsistente. Ótima explicação, mas para ficar claro… isso é resultado de um problema de implementação ou reflete uma sabedoria mais profunda sobre o uso e a passagem de funções? Como as funções anônimas podem corresponder com base nos valores dos argumentos, parece que seria útil também poder corresponder ao número de argumentos (e consistente com a correspondência de padrões de funções em outros lugares).
Cyclone

17
Não é uma restrição de implementação no sentido em que também poderia funcionar como f()(sem o ponto).
José Valim 29/09

6
Você pode corresponder ao número de argumentos usando is_function/2um guarda. is_function(f, 2)cheques tem arity de 2. :)
José Valim

20
Eu teria preferido invocações de funções sem pontos para as funções nomeadas e anônimas. Às vezes, isso é confuso e você esquece se uma função específica era anônima ou nomeada. Também há mais barulho quando se trata de curry.
precisa saber é o seguinte

28
Em Erlang, chamadas de função anônimas e funções regulares já são sintaticamente diferentes: SomeFun()e some_fun(). No Elixir, se removêssemos o ponto, eles seriam os mesmos some_fun()e some_fun()porque as variáveis ​​usam o mesmo identificador como nomes de funções. Daí o ponto.
José Valim

17

Não sei o quanto isso será útil para mais ninguém, mas a maneira como finalmente entendi o conceito foi perceber que as funções do elixir não são funções.

Tudo no elixir é uma expressão. assim

MyModule.my_function(foo) 

não é uma função, mas a expressão retornada executando o código em my_function. Na verdade, existe apenas uma maneira de obter uma "Função" que você pode transmitir como argumento e usar a notação de função anônima.

É tentador referir-se à notação fn ou & como um ponteiro de função, mas na verdade é muito mais. É um fechamento do ambiente circundante.

Se você se perguntar:

Preciso de um ambiente de execução ou de um valor de dados neste local?

E se você precisar de execução, use fn, então a maioria das dificuldades fica muito mais clara.


2
É claro que MyModule.my_function(foo)é uma expressão, mas MyModule.my_function"poderia" ter sido uma expressão que retorna uma função "objeto". Mas já que você precisa contar à aridade, você precisaria de algo parecido MyModule.my_function/1. E acho que eles decidiram que era melhor usar uma &MyModule.my_function(&1) sintaxe que permita expressar a aridade (e serve a outros propósitos também). Ainda não está claro porque é que há um ()operador para funções nomeadas e um .()operador para funções sem nome
RubenLaguna

Eu acho que você quer: &MyModule.my_function/1é assim que você pode distribuí-lo como uma função.
Jnmandal

14

Eu nunca entendi por que as explicações disso são tão complicadas.

É realmente apenas uma distinção excepcionalmente pequena combinada com as realidades da "execução de funções sem parênteses" no estilo Ruby.

Comparar:

def fun1(x, y) do
  x + y
end

Para:

fun2 = fn
  x, y -> x + y
end

Embora ambos sejam apenas identificadores ...

  • fun1é um identificador que descreve uma função nomeada definida com def.
  • fun2 é um identificador que descreve uma variável (que contém uma referência à função).

Considere o que isso significa quando você vê fun1ou fun2em alguma outra expressão? Ao avaliar essa expressão, você chama a função referenciada ou apenas faz referência a um valor sem memória?

Não há uma boa maneira de saber em tempo de compilação. Ruby tem o luxo de inspecionar o espaço para nome da variável para descobrir se uma ligação de variável sombreava uma função em algum momento. Elixir, sendo compilado, não pode realmente fazer isso. É isso que a notação de ponto faz, diz ao Elixir que deve conter uma referência de função e que deve ser chamada.

E isso é realmente difícil. Imagine que não havia uma notação de ponto. Considere este código:

val = 5

if :rand.uniform < 0.5 do
  val = fn -> 5 end
end

IO.puts val     # Does this work?
IO.puts val.()  # Or maybe this?

Dado o código acima, acho bem claro por que você deve dar a dica ao Elixir. Imagine se todas as des-referências de variáveis ​​tivessem que verificar uma função? Como alternativa, imagine que heroísmo seria necessário para sempre inferir que a desreferência variável estava usando uma função?


12

Posso estar errado, já que ninguém o mencionou, mas também fiquei com a impressão de que a razão disso também é a herança ruby ​​de poder chamar funções sem colchetes.

Arity está obviamente envolvido, mas vamos deixar isso de lado por um tempo e usar funções sem argumentos. Em uma linguagem como o javascript, onde os colchetes são obrigatórios, é fácil fazer a diferença entre passar uma função como argumento e chamar a função. Você chama apenas quando usa os colchetes.

my_function // argument
(function() {}) // argument

my_function() // function is called
(function() {})() // function is called

Como você pode ver, nomeá-lo ou não não faz uma grande diferença. Mas elixir e ruby ​​permitem chamar funções sem os colchetes. Essa é uma opção de design que eu pessoalmente gosto, mas tem esse efeito colateral que você não pode usar apenas o nome sem os colchetes, pois pode significar que você deseja chamar a função. É para isso que &serve. Se você deixar o arity appart por um segundo, acrescentar o nome da sua função &significa que deseja explicitamente usar essa função como argumento, e não o que essa função retorna.

Agora, a função anônima é um pouco diferente, pois é usada principalmente como argumento. Novamente, essa é uma opção de design, mas o racional por trás disso é que ela é usada principalmente por iteradores, tipo de funções que aceitam funções como argumentos. Então, obviamente, você não precisa usá-lo &porque eles já são considerados argumentos por padrão. É o propósito deles.

Agora, o último problema é que às vezes você precisa chamá-los em seu código, porque eles nem sempre são usados ​​com um tipo de função de iterador ou você pode estar codificando um iterador. Para a pequena história, como o ruby ​​é orientado a objetos, a principal maneira de fazer isso era usar o callmétodo no objeto. Dessa forma, você pode manter o comportamento dos colchetes não obrigatórios consistente.

my_lambda.call
my_lambda.call()
my_lambda_with_arguments.call :h2g2, 42
my_lambda_with_arguments.call(:h2g2, 42)

Agora, alguém criou um atalho que basicamente se parece com um método sem nome.

my_lambda.()
my_lambda_with_arguments.(:h2g2, 42)

Novamente, essa é uma escolha de design. Agora, o elixir não é orientado a objetos e, portanto, não deve usar o primeiro formulário com certeza. Não posso falar por José, mas parece que a segunda forma foi usada no elixir porque ainda parece uma chamada de função com um caractere extra. É perto o suficiente de uma chamada de função.

Eu não pensei em todos os prós e contras, mas parece que nos dois idiomas você pode se dar bem com os colchetes desde que os torne obrigatórios para funções anônimas. Parece que é:

Parênteses obrigatórios VS Notação ligeiramente diferente

Nos dois casos, você abre uma exceção porque os dois se comportam de maneira diferente. Como existe uma diferença, é melhor deixar óbvio e seguir a notação diferente. Os colchetes obrigatórios pareceriam naturais na maioria dos casos, mas muito confusos quando as coisas não saem conforme o planejado.

Aqui está. Agora, essa pode não ser a melhor explicação do mundo, porque simplifiquei a maioria dos detalhes. Também a maioria são escolhas de design e tentei dar uma razão para elas sem julgá-las. Eu amo elixir, eu amo rubi, gosto das chamadas de função sem colchetes, mas, como você, acho as consequências bastante errôneas de vez em quando.

E no elixir, é apenas esse ponto extra, enquanto no rubi você tem blocos em cima disso. Os blocos são incríveis e estou surpreso com o quanto você pode fazer com apenas blocos, mas eles só funcionam quando você precisa de apenas uma função anônima que é o último argumento. Então, como você deve lidar com outros cenários, aqui vem todo o método / lambda / proc / block confusion.

Enfim ... isso está fora de escopo.


9

Há uma excelente postagem no blog sobre esse comportamento: link

Dois tipos de funções

Se um módulo contiver isso:

fac(0) when N > 0 -> 1;
fac(N)            -> N* fac(N-1).

Você não pode simplesmente cortar e colar isso na casca e obter o mesmo resultado.

É porque há um erro em Erlang. Módulos em Erlang são sequências de FORMS . O shell Erlang avalia uma sequência de EXPRESSÕES . Em Erlang, os FORMS não são EXPRESSÕES .

double(X) -> 2*X.            in an Erlang module is a FORM

Double = fun(X) -> 2*X end.  in the shell is an EXPRESSION

Os dois não são o mesmo. Esse pouco de tolice sempre foi Erlang, mas não percebemos e aprendemos a conviver com ele.

Ponto na chamada fn

iex> f = fn(x) -> 2 * x end
#Function<erl_eval.6.17052888>
iex> f.(10)
20

Na escola, aprendi a chamar funções escrevendo f (10) e não f. (10) - essa é "realmente" uma função com um nome como Shell.f (10) (é uma função definida no shell). A parte do shell é implícito, portanto deve ser chamado de f (10).

Se você deixar assim, espere passar os próximos vinte anos de sua vida explicando o porquê.


Não tenho certeza da utilidade em responder diretamente à pergunta do OP, mas o link fornecido (incluindo a seção de comentários) é realmente uma ótima IMO de leitura para o público-alvo da pergunta, ou seja, aqueles que são novos no & learning Elixir + Erlang .

O link está morto agora :(
AnilRedshift

2

O Elixir possui chaves opcionais para funções, incluindo funções com zero aridade. Vamos ver um exemplo de por que ela torna importante uma sintaxe de chamada separada:

defmodule Insanity do
  def dive(), do: fn() -> 1 end
end

Insanity.dive
# #Function<0.16121902/0 in Insanity.dive/0>

Insanity.dive() 
# #Function<0.16121902/0 in Insanity.dive/0>

Insanity.dive.()
# 1

Insanity.dive().()
# 1

Sem fazer a diferença entre dois tipos de funções, não podemos dizer o que Insanity.divesignifica: obter uma função em si, chamá-la ou também chamar a função anônima resultante.


1

fn ->sintaxe é para usar funções anônimas. Fazer var. () Está apenas dizendo ao elixir que eu quero que você pegue esse var com uma função e execute-o, em vez de se referir ao var como algo apenas mantendo essa função.

O Elixir possui um padrão comum em que, em vez de ter lógica dentro de uma função para ver como algo deve ser executado, combinamos diferentes funções com base no tipo de entrada que temos. Suponho que é por isso que nos referimos às coisas por aridade no function_name/1sentido.

É meio estranho se acostumar a fazer definições de funções abreviadas (func (& 1), etc.), mas útil quando você está tentando canalizar ou manter seu código conciso.


0

Somente o segundo tipo de função parece ser um objeto de primeira classe e pode ser passado como parâmetro para outras funções. Uma função definida em um módulo precisa ser agrupada em um fn. Existe um pouco de açúcar sintático que parece otherfunction(myfunction(&1, &2))facilitar isso, mas por que é necessário em primeiro lugar? Por que não podemos simplesmente fazer otherfunction(myfunction))?

Você pode fazer otherfunction(&myfunction/2)

Como o elixir pode executar funções sem os colchetes (como myfunction), usá- otherfunction(myfunction))lo tentará executar myfunction/0.

Portanto, você precisa usar o operador de captura e especificar a função, incluindo arity, pois você pode ter funções diferentes com o mesmo nome. Assim &myfunction/2,.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.