Dlaczego istnieją dwa rodzaje funkcji w Elixir?


279

Uczę się Elixir i zastanawiam się, dlaczego ma dwa typy definicji funkcji:

  • funkcje zdefiniowane w module za pomocą def, zwane usingmyfunction(param1, param2)
  • funkcje anonimowe zdefiniowane za pomocą fn, wywoływane za pomocąmyfn.(param1, param2)

Tylko drugi rodzaj funkcji wydaje się być obiektem pierwszej klasy i może być przekazany jako parametr do innych funkcji. Funkcja zdefiniowana w module musi być zapakowana w fn. Jest trochę cukru syntaktycznego, który wygląda tak, otherfunction(myfunction(&1, &2))aby było to łatwe, ale dlaczego jest to konieczne? Dlaczego nie możemy tego zrobić otherfunction(myfunction))? Czy pozwala tylko na wywoływanie funkcji modułu bez nawiasów, jak w Ruby? Wygląda na to, że odziedziczyła tę cechę po Erlangu, który ma również funkcje modułu i zabawy, więc czy w rzeczywistości pochodzi z tego, jak Erlang VM działa wewnętrznie?

Czy jest jakaś korzyść z posiadania dwóch rodzajów funkcji i konwersji z jednego typu na inny w celu przekazania ich do innych funkcji? Czy jest korzyść z posiadania dwóch różnych oznaczeń do wywoływania funkcji?

Odpowiedzi:


386

Aby wyjaśnić nazewnictwo, obie są funkcjami. Jedna jest funkcją nazwaną, a druga anonimową. Ale masz rację, działają one nieco inaczej i zamierzam zilustrować, dlaczego tak działają.

Zacznijmy drugim fn. fnjest zamknięciem, podobnym do lambdaw Ruby. Możemy go utworzyć w następujący sposób:

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

Funkcja może mieć także wiele klauzul:

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

A teraz spróbujmy czegoś innego. Spróbujmy zdefiniować różne klauzule, oczekując innej liczby argumentów:

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

O nie! Otrzymujemy błąd! Nie możemy mieszać klauzul, które oczekują innej liczby argumentów. Funkcja zawsze ma ustaloną aranżację.

Porozmawiajmy teraz o nazwanych funkcjach:

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

Zgodnie z oczekiwaniami mają nazwę i mogą również otrzymać pewne argumenty. Nie są to jednak zamknięcia:

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

Ten kod się nie skompiluje, ponieważ za każdym razem, gdy widzisz a def, otrzymujesz pusty zakres zmiennej. To ważna różnica między nimi. Szczególnie podoba mi się to, że każda nazwana funkcja zaczyna się od czystej tablicy i nie miesza się zmiennych różnych zakresów. Masz wyraźną granicę.

Mogliśmy pobrać powyższą funkcję hello jako funkcję anonimową. Sam o tym wspomniałeś:

other_function(&hello(&1))

A potem zapytałeś, dlaczego nie mogę tak po prostu przekazać, hellojak w innych językach? Wynika to z faktu, że funkcje w Elixir są identyfikowane według nazwy i pochodzenia. Tak więc funkcja, która oczekuje dwóch argumentów, jest inną funkcją niż ta, która oczekuje trzech, nawet jeśli miałyby tę samą nazwę. Gdybyśmy po prostu zdali hello, nie mielibyśmy pojęcia, co hellonaprawdę miałeś na myśli. Ten z dwoma, trzema lub czterema argumentami? To jest dokładnie ten sam powód, dla którego nie możemy stworzyć anonimowej funkcji z klauzulami o różnych aracjach.

Od Elixir v0.10.1 mamy składnię do przechwytywania nazwanych funkcji:

&hello/1

To uchwyci lokalną funkcję o nazwie hello z arity 1. W całym języku i jego dokumentacji bardzo często identyfikuje się funkcje w tej hello/1składni.

Z tego powodu Elixir używa kropki do wywoływania funkcji anonimowych. Ponieważ nie możesz po prostu przekazać hellojako funkcji, zamiast tego musisz jawnie ją uchwycić, istnieje naturalne rozróżnienie między funkcjami nazwanymi i anonimowymi, a odrębna składnia wywoływania każdej z nich sprawia, że ​​wszystko jest nieco bardziej wyraźne (Lispers byłby z tym zaznajomiony z powodu dyskusji Lisp 1 vs. Lisp 2).

Ogólnie są to powody, dla których mamy dwie funkcje i dlaczego zachowują się inaczej.


30
Uczę się również eliksiru i jest to pierwszy problem, który mi się przydarzył, który mnie przerwał, coś w tym rodzaju wydawało się niespójne. Świetne wyjaśnienie, ale dla jasności… czy jest to wynikiem problemu z implementacją, czy też odzwierciedla głębszą wiedzę na temat używania i przekazywania funkcji? Ponieważ funkcje anonimowe mogą się dopasowywać w oparciu o wartości argumentów, wydaje się, że warto byłoby dopasować również liczbę argumentów (i zgodne z dopasowaniem wzorca funkcji w innym miejscu).
Cyklon

17
Nie jest to ograniczenie implementacyjne w tym sensie, że może również działać jako f()(bez kropki).
José Valim

6
Możesz dopasować liczbę argumentów, używając is_function/2osłony. is_function(f, 2)sprawdza, czy ma arity 2. :)
José Valim

20
Wolałbym wywoływać funkcje bez kropek zarówno dla funkcji nazwanej, jak i anonimowej. Czasami jest to mylące i zapominasz, czy dana funkcja była anonimowa lub nazwana. Jest również więcej hałasu, jeśli chodzi o curry.
CMCDragonkai

28
W Erlang anonimowe wywołania funkcji i funkcje regularne są już składniowo różne: SomeFun()i some_fun(). W Elixir, jeśli usunęliśmy kropkę, byłyby takie same some_fun()i some_fun()dlatego zmienne używać tego samego identyfikatora jako nazwy funkcji. Stąd kropka.
José Valim

17

Nie wiem, jak użyteczne będzie to dla kogokolwiek innego, ale sposób, w jaki ostatecznie opuściłem głowę, to uświadomienie sobie, że funkcje eliksiru nie są funkcjami.

Wszystko w eliksir jest wyrazem. Więc

MyModule.my_function(foo) 

nie jest funkcją, ale wyrażenie zwracane przez wykonanie kodu w my_function. W rzeczywistości istnieje tylko jeden sposób na uzyskanie „funkcji”, którą można przekazać jako argument, a mianowicie użycie anonimowej notacji funkcji.

Kuszące jest odwoływanie się do fn lub notacji jako wskaźnika funkcji, ale tak naprawdę jest to znacznie więcej. To zamknięcie otaczającego środowiska.

Jeśli zadajesz sobie pytanie:

Czy potrzebuję środowiska wykonawczego lub wartości danych w tym miejscu?

A jeśli potrzebujesz wykonania, użyj fn, wówczas większość trudności staje się znacznie wyraźniejsza.


2
Jest jasne, że MyModule.my_function(foo)jest to wyrażenie, ale MyModule.my_function„mogło” być wyrażeniem, które zwraca funkcję „obiekt”. Ale ponieważ musisz powiedzieć arsenowi, potrzebujesz czegoś takiego MyModule.my_function/1. I myślę, że zdecydowali, że lepiej jest użyć &MyModule.my_function(&1) składni, która pozwala wyrazić arity (i służy również innym celom). Nadal niejasne, dlaczego istnieje ()operator dla nazwanych funkcji i .()operator dla nienazwanych funkcji
RubenLaguna

Myślę, że chcesz: w ten &MyModule.my_function/1sposób możesz to przekazać jako funkcję.
jnmandal

14

Nigdy nie zrozumiałem, dlaczego wyjaśnienia tego są tak skomplikowane.

To naprawdę tylko wyjątkowo małe rozróżnienie w połączeniu z realiami „wykonywania funkcji bez parenów” w stylu Rubiego.

Porównać:

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

Do:

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

Oba są tylko identyfikatorami ...

  • fun1to identyfikator opisujący nazwaną funkcję zdefiniowaną za pomocą def.
  • fun2 to identyfikator opisujący zmienną (która zawiera odniesienie do funkcji).

Zastanów się, co to oznacza, kiedy widzisz fun1lub fun2w innym wyrażeniu? Czy podczas oceny tego wyrażenia wywołujesz funkcję, do której się odwołujesz, czy po prostu odwołujesz się do wartości z pamięci?

Nie ma dobrego sposobu na poznanie w czasie kompilacji. Ruby ma luksus introspekcji przestrzeni nazw zmiennych, aby dowiedzieć się, czy powiązanie zmiennej przesłaniało funkcję w pewnym momencie. Kompilowany eliksir tak naprawdę nie może tego zrobić. To właśnie robi notacja kropkowa, mówi Elixirowi, że powinien zawierać odwołanie do funkcji i że powinien zostać wywołany.

I to jest naprawdę trudne. Wyobraź sobie, że nie było notacji kropkowej. Rozważ ten kod:

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?

Biorąc pod uwagę powyższy kod, myślę, że jest całkiem jasne, dlaczego musisz dać Elixirowi podpowiedź. Wyobraź sobie, że każde odwołanie do zmiennej musiało sprawdzać funkcję? Alternatywnie, wyobraź sobie, jaka heroika byłaby konieczna, aby zawsze wywnioskować, że zmienne dereferencje używały funkcji?


12

Mogę się mylić, ponieważ nikt o tym nie wspominał, ale miałem również wrażenie, że powodem tego jest także rubinowe dziedzictwo możliwości wywoływania funkcji bez nawiasów.

Arity jest oczywiście zaangażowane, ale odłóżmy to na chwilę i używaj funkcji bez argumentów. W języku takim jak javascript, w którym nawiasy są obowiązkowe, łatwo jest odróżnić przekazanie funkcji jako argumentu od wywołania funkcji. Nazywasz to tylko wtedy, gdy używasz nawiasów.

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

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

Jak widać, nazywanie go lub nie ma większego znaczenia. Ale eliksir i rubin pozwalają wywoływać funkcje bez nawiasów. Jest to wybór projektowy, który osobiście mi się podoba, ale ma taki efekt uboczny, że nie można użyć samej nazwy bez nawiasów, ponieważ może to oznaczać, że chcesz wywołać funkcję. Po to &jest. Jeśli pozostawisz arity appart na sekundę, dodanie nazwy funkcji &oznacza, że ​​jawnie chcesz użyć tej funkcji jako argumentu, a nie tego, co ta funkcja zwraca.

Teraz funkcja anonimowa różni się tym, że jest używana głównie jako argument. Ponownie jest to wybór projektowy, ale uzasadnia to fakt, że jest używany głównie przez funkcje iteracyjne, które przyjmują funkcje jako argumenty. Więc oczywiście nie musisz używać, &ponieważ są już domyślnie uważane za argumenty. To jest ich cel.

Ostatni problem polega na tym, że czasami trzeba wywoływać je w kodzie, ponieważ nie zawsze są one używane z funkcją typu iteratora lub może sam kodujesz iterator. W przypadku małej historii, ponieważ ruby ​​jest zorientowany obiektowo, głównym sposobem na to było użycie callmetody na obiekcie. W ten sposób można zachować spójność zachowania nieobowiązkowych nawiasów.

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

Teraz ktoś wymyślił skrót, który w zasadzie wygląda jak metoda bez nazwy.

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

Ponownie jest to wybór projektowy. Teraz eliksir nie jest zorientowany obiektowo i dlatego na pewno nie używa pierwszej formy. Nie mogę mówić za José, ale wygląda na to, że w eliksiru zastosowano drugą formę, ponieważ nadal wygląda jak wywołanie funkcji z dodatkową postacią. Jest wystarczająco blisko do wywołania funkcji.

Nie myślałem o wszystkich zaletach i wadach, ale wygląda na to, że w obu językach można uzyskać tylko nawiasy, pod warunkiem, że nawiasy są obowiązkowe dla funkcji anonimowych. Wygląda na to, że to:

Nawiasy obowiązkowe VS Nieznacznie inna notacja

W obu przypadkach robisz wyjątek, ponieważ oba zachowują się inaczej. Ponieważ istnieje różnica, równie dobrze możesz to uczynić oczywistym i wybrać inną notację. Obowiązkowe nawiasy klamrowe wyglądałyby naturalnie w większości przypadków, ale były bardzo mylące, gdy rzeczy nie potoczyły się zgodnie z planem.

Proszę bardzo. To może nie być najlepsze wytłumaczenie na świecie, ponieważ uprościłem większość szczegółów. Większość z nich to wybory projektowe i starałem się podać ich powód, nie oceniając ich. Uwielbiam eliksir, uwielbiam rubin, lubię wywołania funkcji bez nawiasów, ale podobnie jak ty, konsekwencje są czasem mylące.

A w eliksirze jest to tylko ta dodatkowa kropka, podczas gdy w rubinie masz na sobie bloki. Bloki są niesamowite i jestem zaskoczony, jak wiele możesz zrobić za pomocą samych bloków, ale działają one tylko wtedy, gdy potrzebujesz tylko jednej anonimowej funkcji, która jest ostatnim argumentem. Następnie, ponieważ powinieneś być w stanie poradzić sobie z innymi scenariuszami, oto cała metoda / zamieszanie / lambda / proc / block.

W każdym razie ... to jest poza zakresem.


9

Istnieje doskonały post na blogu na temat tego zachowania: link

Dwa rodzaje funkcji

Jeśli moduł zawiera to:

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

Nie możesz po prostu wyciąć i wkleić tego do skorupy i uzyskać ten sam wynik.

To dlatego, że w Erlangu jest błąd. Moduły w Erlang są sekwencjami FORM . Powłoka Erlanga ocenia sekwencję WYRAŻEŃ . W Erlang FORMULARZE nie są EKSPRESJAMI .

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

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

Obie nie są takie same. Ta odrobina głupoty była Erlangiem na zawsze, ale nie zauważyliśmy tego i nauczyliśmy się z tym żyć.

Dot w dzwonieniu fn

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

W szkole nauczyłem się wywoływać funkcje pisząc f (10), a nie f. (10) - jest to „naprawdę” funkcja o nazwie takiej jak Shell.f (10) (jest to funkcja zdefiniowana w powłoce) Część powłoki jest niejawne, dlatego należy go nazwać f (10).

Jeśli tak porzucisz, spodziewaj się, że spędzisz następne dwadzieścia lat swojego życia, tłumacząc dlaczego.


Nie jestem pewien przydatności w bezpośredniej odpowiedzi na pytanie PO, ale podany link (w tym sekcja komentarzy) jest w rzeczywistości świetną lekturą IMO dla odbiorców docelowych pytania, tj. Tych z nas, którzy dopiero zaczynają naukę Elixir + Erlang i uczą się .

Link już nie działa :(
AnilRedshift

2

Eliksir ma opcjonalne nawiasy klamrowe dla funkcji, w tym funkcji o wartości 0. Zobaczmy przykład, dlaczego ważna jest osobna składnia wywoływania:

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

Nie czyniąc różnicy między 2 typami funkcji, nie możemy powiedzieć, co Insanity.diveoznacza: uzyskanie samej funkcji, wywołanie jej, czy też wywołanie wynikowej funkcji anonimowej.


1

fn ->składnia służy do korzystania z funkcji anonimowych. Wykonanie var. () Po prostu mówi eliksirowi, że chcę, abyś wziął ten var z func i uruchomił go zamiast odwoływać się do var jako czegoś, co po prostu pełni tę funkcję.

Eliksir ma ten wspólny wzorzec, w którym zamiast logiki wewnątrz funkcji, aby zobaczyć, jak coś powinno się wykonać, wzorujemy dopasowywanie różnych funkcji w zależności od tego, jaki rodzaj danych wejściowych mamy. Zakładam, że właśnie dlatego odnosimy się do rzeczy ze względu na podobieństwo wfunction_name/1 sensie .

Dziwnie jest przyzwyczaić się do tworzenia skrótowych definicji funkcji (func (i 1) itp.), Ale jest to przydatne, gdy próbujesz potokować lub zachować zwięzłość kodu.


0

Tylko drugi rodzaj funkcji wydaje się być obiektem pierwszej klasy i może być przekazany jako parametr do innych funkcji. Funkcja zdefiniowana w module musi być zapakowana w fn. Jest trochę cukru syntaktycznego, który wygląda tak, otherfunction(myfunction(&1, &2))aby było to łatwe, ale dlaczego jest to konieczne? Dlaczego nie możemy tego zrobić otherfunction(myfunction))?

Możesz to zrobić otherfunction(&myfunction/2)

Ponieważ eliksir może wykonywać funkcje bez nawiasów kwadratowych (jak myfunction), użycie otherfunction(myfunction))go spróbuje wykonać myfunction/0.

Musisz więc użyć operatora przechwytywania i określić funkcję, w tym arity, ponieważ możesz mieć różne funkcje o tej samej nazwie. Tak więc &myfunction/2.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.