Perché ci sono due tipi di funzioni nell'elisir?


279

Sto imparando Elisir e mi chiedo perché abbia due tipi di definizioni delle funzioni:

  • funzioni definite in un modulo con def, chiamate usandomyfunction(param1, param2)
  • funzioni anonime definite con fn, chiamate usandomyfn.(param1, param2)

Solo il secondo tipo di funzione sembra essere un oggetto di prima classe e può essere passato come parametro ad altre funzioni. Una funzione definita in un modulo deve essere racchiusa in a fn. C'è un po 'di zucchero sintattico che sembra otherfunction(myfunction(&1, &2))per renderlo facile, ma perché è necessario in primo luogo? Perché non possiamo semplicemente fare otherfunction(myfunction))? Permette solo di chiamare le funzioni del modulo senza parentesi come in Ruby? Sembra aver ereditato questa caratteristica da Erlang che ha anche funzioni e divertimenti per i moduli, quindi in realtà viene da come la VM Erlang lavora internamente?

Ha qualche vantaggio avere due tipi di funzioni e convertirle da un tipo all'altro per passarle ad altre funzioni? C'è un vantaggio che ha due diverse notazioni per chiamare le funzioni?

Risposte:


386

Giusto per chiarire la denominazione, sono entrambe le funzioni. Uno è una funzione con nome e l'altro è anonimo. Ma hai ragione, funzionano in modo leggermente diverso e ho intenzione di illustrare perché funzionano così.

Cominciamo con il secondo fn,. fnè una chiusura, simile a una lambdain Ruby. Possiamo crearlo come segue:

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

Una funzione può avere anche più clausole:

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

Ora proviamo qualcosa di diverso. Proviamo a definire diverse clausole aspettandoci un diverso numero di argomenti:

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

Oh no! Abbiamo ricevuto un errore! Non possiamo mescolare clausole che prevedono un diverso numero di argomenti. Una funzione ha sempre un'arità fissa.

Ora parliamo delle funzioni nominate:

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

Come previsto, hanno un nome e possono anche ricevere alcuni argomenti. Tuttavia, non sono chiusure:

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

Questo codice non verrà compilato perché ogni volta che vedi un def, ottieni un ambito variabile vuoto. Questa è una differenza importante tra loro. Mi piace in particolare il fatto che ogni funzione denominata inizia con una lavagna pulita e non si confondono le variabili di ambiti diversi. Hai un chiaro confine.

Potremmo recuperare la funzione ciao denominata sopra come una funzione anonima. L'hai menzionato tu stesso:

other_function(&hello(&1))

E poi mi hai chiesto, perché non posso semplicemente passarlo hellocome in altre lingue? Questo perché le funzioni in Elisir sono identificate per nome e arità. Quindi una funzione che prevede due argomenti è una funzione diversa da una che prevede tre argomenti, anche se avevano lo stesso nome. Quindi, se semplicemente passassimo hello, non avremmo idea di cosa hellointendevi davvero. Quello con due, tre o quattro argomenti? Questo è esattamente lo stesso motivo per cui non possiamo creare una funzione anonima con clausole con diverse arità.

A partire da Elixir v0.10.1, abbiamo una sintassi per acquisire funzioni con nome:

&hello/1

Che catturerà la funzione denominata locale ciao con arity 1. In tutta la lingua e la sua documentazione, è molto comune identificare le funzioni in questa hello/1sintassi.

Questo è anche il motivo per cui Elisir utilizza un punto per chiamare funzioni anonime. Dal momento che non puoi semplicemente passare in hellogiro come funzione, invece devi catturarla esplicitamente, c'è una naturale distinzione tra funzioni nominate e anonime e una sintassi distinta per chiamare ognuna rende tutto un po 'più esplicito (Lispers avrebbe familiarità con questo a causa della discussione su Lisp 1 vs. Lisp 2).

Nel complesso, questi sono i motivi per cui abbiamo due funzioni e perché si comportano diversamente.


30
Sto anche imparando l'Elisir, e questo è il primo problema che ho riscontrato che mi ha fatto mettere in pausa, qualcosa al riguardo mi è sembrato incoerente. Grande spiegazione, ma per essere chiari ... questo è il risultato di un problema di implementazione o riflette una saggezza più profonda riguardo all'uso e al passaggio delle funzioni? Poiché le funzioni anonime possono corrispondere in base ai valori degli argomenti, sembra che sarebbe utile essere in grado di abbinare anche il numero di argomenti (e coerentemente con la corrispondenza del modello di funzione altrove).
Ciclone,

17
Non è un vincolo di implementazione, nel senso che potrebbe anche funzionare come f()(senza il punto).
José Valim,

6
Puoi abbinare il numero di argomenti usando is_function/2in una guardia. is_function(f, 2)controlla che abbia un'arità di 2. :)
José Valim,

20
Avrei preferito invocazioni di funzioni senza punti sia per la funzione nominata che per quella anonima. A volte lo confonde e si dimentica se una determinata funzione era anonima o denominata. C'è anche più rumore quando si tratta di curry.
CMCDragonkai,

28
In Erlang, le chiamate di funzioni anonime e le funzioni regolari sono già sintatticamente diverse: SomeFun()e some_fun(). In Elisir, se rimuovessimo il punto, sarebbero gli stessi some_fun()e some_fun()poiché le variabili usano lo stesso identificatore dei nomi delle funzioni. Da qui il punto.
José Valim,

17

Non so quanto questo sarà utile a chiunque altro, ma il modo in cui ho finalmente avvolto la mia testa attorno al concetto è stato rendermi conto che le funzioni di elisir non sono funzioni.

Tutto nell'elisir è un'espressione. Così

MyModule.my_function(foo) 

non è una funzione ma l'espressione restituita eseguendo il codice in my_function. In realtà esiste solo un modo per ottenere una "Funzione" che puoi passare come argomento e che è usare la notazione di funzione anonima.

È allettante fare riferimento a fn o & notation come puntatore a funzione, ma in realtà è molto di più. È una chiusura dell'ambiente circostante.

Se ti chiedi:

Ho bisogno di un ambiente di esecuzione o di un valore di dati in questo punto?

E se hai bisogno di esecuzione usa fn, allora la maggior parte delle difficoltà diventa molto più chiara.


2
È chiaro che MyModule.my_function(foo)è un'espressione ma MyModule.my_function"potrebbe" essere stata un'espressione che restituisce una funzione "oggetto". Ma dal momento che devi dire l'arità, avresti invece bisogno di qualcosa di simile MyModule.my_function/1. E immagino che abbiano deciso che fosse meglio usare una &MyModule.my_function(&1) sintassi che permettesse di esprimere l'arity (e serve anche ad altri scopi). Non è ancora chiaro il motivo per cui esiste un ()operatore per le funzioni con nome e un .()operatore per le funzioni senza nome
RubenLaguna

Penso che tu voglia: &MyModule.my_function/1ecco come puoi passarlo in giro come funzione.
jnmandal,

14

Non ho mai capito perché le spiegazioni siano così complicate.

È davvero solo una distinzione eccezionalmente piccola combinata con la realtà dell'esecuzione di funzioni in stile Ruby senza parentesi.

Confrontare:

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

Per:

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

Mentre entrambi sono solo identificatori ...

  • fun1è un identificatore che descrive una funzione denominata definita con def.
  • fun2 è un identificatore che descrive una variabile (che sembra contenere un riferimento alla funzione).

Considera cosa significa quando vedi fun1o fun2in qualche altra espressione? Quando valuti quell'espressione, chiami la funzione referenziata o fai semplicemente riferimento a un valore di memoria insufficiente?

Non c'è un buon modo per sapere al momento della compilazione. Ruby ha il lusso di introspettare lo spazio dei nomi delle variabili per scoprire se un'associazione di variabili ha oscurato una funzione in un determinato momento. L'elisir, in fase di compilazione, non può davvero farlo. Questo è ciò che fa la notazione punto, dice all'elisir che dovrebbe contenere un riferimento di funzione e che dovrebbe essere chiamato.

E questo è davvero difficile. Immagina che non ci fosse una notazione a punti. Considera questo codice:

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?

Dato il codice sopra, penso che sia abbastanza chiaro il motivo per cui devi dare il suggerimento a Elixir. Immagina se ogni de-riferimento variabile dovesse verificare una funzione? In alternativa, immagina quale eroismo sarebbe necessario per dedurre sempre che la dereferenza variabile stava usando una funzione?


12

Potrei sbagliarmi dal momento che nessuno lo ha menzionato, ma avevo anche l'impressione che la ragione di ciò sia anche l'eredità rubina di poter chiamare funzioni senza parentesi.

Arity è ovviamente coinvolto, ma lascialo da parte per un po 'e usa le funzioni senza argomenti. In un linguaggio come javascript in cui le parentesi sono obbligatorie, è facile fare la differenza tra passare una funzione come argomento e chiamare la funzione. Lo chiami solo quando usi le parentesi.

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

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

Come puoi vedere, nominarlo o meno non fa una grande differenza. Ma elisir e ruby ​​ti consentono di chiamare funzioni senza parentesi. Questa è una scelta di design che mi piace personalmente ma ha questo effetto collaterale che non puoi usare solo il nome senza le parentesi perché potrebbe significare che vuoi chiamare la funzione. Questo è ciò che &serve. Se lasci arity appart per un secondo, anteporre il nome della tua funzione &significa che vuoi esplicitamente usare questa funzione come argomento, non ciò che questa funzione restituisce.

Ora la funzione anonima è leggermente diversa in quanto viene utilizzata principalmente come argomento. Ancora una volta questa è una scelta progettuale, ma il razionale dietro è che è principalmente usata da iteratori di funzioni che prendono le funzioni come argomenti. Quindi ovviamente non è necessario utilizzarlo &perché sono già considerati argomenti per impostazione predefinita. È il loro scopo.

Ora l'ultimo problema è che a volte devi chiamarli nel tuo codice, perché non sono sempre usati con un tipo di funzione iteratore, o potresti codificare tu stesso un iteratore. Per la piccola storia, poiché ruby ​​è orientato agli oggetti, il modo principale per farlo è stato quello di utilizzare il callmetodo sull'oggetto. In questo modo, è possibile mantenere coerente il comportamento delle parentesi non obbligatorie.

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

Ora qualcuno ha trovato una scorciatoia che in pratica sembra un metodo senza nome.

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

Ancora una volta, questa è una scelta di design. Ora elisir non è orientato agli oggetti e quindi chiama di sicuro non usare il primo modulo. Non posso parlare per José ma sembra che la seconda forma sia stata usata nell'elisir perché sembra ancora una chiamata di funzione con un carattere in più. È abbastanza vicino a una chiamata di funzione.

Non ho pensato a tutti i pro e contro, ma sembra che in entrambe le lingue potresti cavartela con le parentesi solo se rendi obbligatorie le parentesi per le funzioni anonime. Sembra che sia:

Parentesi obbligatorie VS Notazione leggermente diversa

In entrambi i casi fai un'eccezione perché fai in modo che entrambi si comportino diversamente. Poiché c'è una differenza, potresti anche renderlo ovvio e scegliere la diversa notazione. Le parentesi obbligatorie sembrerebbero naturali nella maggior parte dei casi, ma molto confuse quando le cose non vanno come previsto.

Ecco qui. Ora questa potrebbe non essere la migliore spiegazione al mondo perché ho semplificato la maggior parte dei dettagli. Inoltre, la maggior parte sono scelte progettuali e ho cercato di dare una ragione per loro senza giudicarle. Adoro l'elisir, adoro il rubino, mi piacciono le chiamate di funzione senza parentesi, ma come te, trovo le conseguenze abbastanza fuorvianti di tanto in tanto.

E nell'elisir, è proprio questo punto in più, mentre nel rubino hai dei blocchi sopra questo. I blocchi sono fantastici e sono sorpreso di quanto tu possa fare con solo i blocchi, ma funzionano solo quando hai bisogno di una sola funzione anonima che è l'ultimo argomento. Quindi, poiché dovresti essere in grado di gestire altri scenari, ecco che arriva l'intero metodo / lambda / proc / block confusion.

Comunque ... questo è fuori portata.


9

C'è un eccellente post sul blog su questo comportamento: link

Due tipi di funzioni

Se un modulo contiene questo:

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

Non puoi semplicemente tagliarlo e incollarlo nella shell e ottenere lo stesso risultato.

È perché c'è un bug in Erlang. I moduli in Erlang sono sequenze di FORME . La shell Erlang valuta una sequenza di EXPRESSIONS . In Erlang le FORME non sono ESPRESSIONI .

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

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

I due non sono la stessa cosa. Questo po 'di stupidità è stato Erlang per sempre, ma non l'abbiamo notato e abbiamo imparato a conviverci.

Punto in chiamata fn

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

A scuola ho imparato a chiamare le funzioni scrivendo f (10) non f. (10) - questa è “davvero” una funzione con un nome come Shell.f (10) (è una funzione definita nella shell) La parte shell è implicito, quindi dovrebbe essere chiamato f (10).

Se lo lasci così, aspettati di passare i prossimi vent'anni della tua vita a spiegare perché.


Non sono sicuro dell'utilità di rispondere direttamente alla domanda OP, ma il link fornito (compresa la sezione commenti) è in realtà un'ottima lettura IMO per il pubblico di destinazione della domanda, vale a dire quelli di noi che non conoscono e imparano Elisir + Erlang .

Il link è morto ora :(
AnilRedshift

2

L'elisir dispone di parentesi graffe opzionali per le funzioni, incluse le funzioni con 0 arity. Vediamo un esempio del perché rende importante una sintassi di chiamata separata:

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

Senza fare la differenza tra 2 tipi di funzioni, non possiamo dire che cosa Insanity.divesignifichi: ottenere una funzione stessa, chiamarla o anche chiamare la funzione anonima risultante.


1

fn ->la sintassi è per l'utilizzo di funzioni anonime. Fare var. () Sta semplicemente dicendo all'elisir che voglio che tu prenda quella var con una funzione e la esegua invece di riferirsi alla var come qualcosa che tiene semplicemente quella funzione.

L'elisir ha un modello comune in cui invece di avere la logica all'interno di una funzione per vedere come qualcosa dovrebbe essere eseguito, modelliamo le diverse funzioni in base al tipo di input che abbiamo. Presumo che sia per questo che ci riferiamo alle cose per arità nel function_name/1senso.

È un po 'strano abituarsi a fare definizioni di funzioni stenografiche (func (& 1), ecc.), Ma utile quando si tenta di eseguire il pipe o mantenere il codice conciso.


0

Solo il secondo tipo di funzione sembra essere un oggetto di prima classe e può essere passato come parametro ad altre funzioni. Una funzione definita in un modulo deve essere racchiusa in un fn. C'è un po 'di zucchero sintattico che sembra otherfunction(myfunction(&1, &2))per renderlo facile, ma perché è necessario in primo luogo? Perché non possiamo semplicemente fare otherfunction(myfunction))?

Tu puoi fare otherfunction(&myfunction/2)

Dal momento che elisir può eseguire funzioni senza parentesi (come myfunction), usandolo otherfunction(myfunction))tenterà di eseguire myfunction/0.

Pertanto, è necessario utilizzare l'operatore di acquisizione e specificare la funzione, incluso arity, poiché è possibile avere funzioni diverse con lo stesso nome. Così, &myfunction/2.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.