Warum gibt es in Elixir zwei Arten von Funktionen?


279

Ich lerne Elixier und frage mich, warum es zwei Arten von Funktionsdefinitionen gibt:

  • Funktionen, die in einem Modul mit definiert sind, werden mit defusing aufgerufenmyfunction(param1, param2)
  • anonyme Funktionen definiert mit fn, aufgerufen mitmyfn.(param1, param2)

Nur die zweite Art von Funktion scheint ein erstklassiges Objekt zu sein und kann als Parameter an andere Funktionen übergeben werden. Eine in einem Modul definierte Funktion muss in a eingeschlossen werden fn. Es gibt etwas syntaktischen Zucker, der aussieht otherfunction(myfunction(&1, &2)), um das zu vereinfachen, aber warum ist er überhaupt notwendig? Warum können wir das nicht einfach tun otherfunction(myfunction))? Ist es nur möglich, Modulfunktionen ohne Klammern wie in Ruby aufzurufen? Es scheint dieses Merkmal von Erlang geerbt zu haben, das auch Modulfunktionen und Spaß hat. Kommt es also tatsächlich davon, wie die Erlang-VM intern funktioniert?

Hat es einen Vorteil, zwei Arten von Funktionen zu haben und von einem Typ in einen anderen zu konvertieren, um sie an andere Funktionen zu übergeben? Gibt es einen Vorteil, wenn zwei verschiedene Notationen zum Aufrufen von Funktionen vorhanden sind?

Antworten:


386

Um die Benennung zu verdeutlichen, sind beide Funktionen. Eine ist eine benannte Funktion und die andere ist eine anonyme. Aber Sie haben Recht, sie funktionieren etwas anders und ich werde veranschaulichen, warum sie so funktionieren.

Beginnen wir mit der zweiten , fn. fnist ein Verschluss, ähnlich einem lambdain Ruby. Wir können es wie folgt erstellen:

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

Eine Funktion kann auch mehrere Klauseln enthalten:

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

Versuchen wir jetzt etwas anderes. Versuchen wir, verschiedene Klauseln zu definieren, die eine unterschiedliche Anzahl von Argumenten erwarten:

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

Ach nein! Wir bekommen einen Fehler! Wir können keine Klauseln mischen, die eine andere Anzahl von Argumenten erwarten. Eine Funktion hat immer eine feste Arität.

Lassen Sie uns nun über die genannten Funktionen sprechen:

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

Wie erwartet haben sie einen Namen und können auch einige Argumente erhalten. Sie sind jedoch keine Schließungen:

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

Dieser Code kann nicht kompiliert werden, da jedes Mal, wenn Sie einen sehen def, ein leerer Variablenbereich angezeigt wird. Das ist ein wichtiger Unterschied zwischen ihnen. Mir gefällt besonders die Tatsache, dass jede benannte Funktion mit einer sauberen Tabelle beginnt und Sie nicht die Variablen verschiedener Bereiche miteinander verwechseln. Sie haben eine klare Grenze.

Wir könnten die oben genannte Hallo-Funktion als anonyme Funktion abrufen. Sie haben es selbst erwähnt:

other_function(&hello(&1))

Und dann hast du gefragt, warum ich es nicht einfach so weitergeben kann hellowie in anderen Sprachen? Das liegt daran, dass Funktionen in Elixir durch Name und Arität identifiziert werden . Eine Funktion, die zwei Argumente erwartet, ist also eine andere Funktion als eine, die drei erwartet, selbst wenn sie denselben Namen haben. Wenn wir also einfach bestanden hätten hello, hätten wir keine Ahnung, was helloSie eigentlich gemeint haben. Der mit zwei, drei oder vier Argumenten? Dies ist genau der gleiche Grund, warum wir keine anonyme Funktion mit Klauseln mit unterschiedlichen Aritäten erstellen können.

Seit Elixir v0.10.1 haben wir eine Syntax zum Erfassen benannter Funktionen:

&hello/1

Dadurch wird die lokal benannte Funktion Hallo mit Arität 1 erfasst. In der gesamten Sprache und ihrer Dokumentation ist es sehr häufig, Funktionen in dieser hello/1Syntax zu identifizieren .

Aus diesem Grund verwendet Elixir auch einen Punkt zum Aufrufen anonymer Funktionen. Da Sie nicht einfach helloals Funktion weitergeben können, sondern diese explizit erfassen müssen, gibt es eine natürliche Unterscheidung zwischen benannten und anonymen Funktionen, und eine unterschiedliche Syntax für den Aufruf macht jede Funktion etwas expliziter (Lispers wäre damit vertraut aufgrund der Diskussion zwischen Lisp 1 und Lisp 2).

Insgesamt sind dies die Gründe, warum wir zwei Funktionen haben und warum sie sich unterschiedlich verhalten.


30
Ich lerne auch Elixier, und dies ist das erste Problem, auf das ich gestoßen bin, das mir eine Pause gab. Etwas daran schien einfach inkonsistent zu sein. Gute Erklärung, aber um klar zu sein ... ist dies das Ergebnis eines Implementierungsproblems oder spiegelt es tiefere Weisheiten hinsichtlich der Verwendung und Weitergabe von Funktionen wider? Da anonyme Funktionen basierend auf Argumentwerten übereinstimmen können, scheint es nützlich zu sein, auch die Anzahl der Argumente abgleichen zu können (und mit dem Funktionsmusterabgleich an anderer Stelle übereinzustimmen).
Zyklon

17
Es ist keine Implementierungsbeschränkung in dem Sinne, dass es auch als f()(ohne Punkt) funktionieren könnte .
José Valim

6
Sie können die Anzahl der Argumente mithilfe is_function/2eines Schutzes abgleichen. is_function(f, 2)prüft,
ob

20
Ich hätte Funktionsaufrufe ohne Punkte sowohl für benannte als auch für anonyme Funktionen bevorzugt. Es macht es manchmal verwirrend und Sie vergessen, ob eine bestimmte Funktion anonym oder benannt war. Es gibt auch mehr Lärm, wenn es um Curry geht.
CMCDragonkai

28
In Erlang unterscheiden sich anonyme Funktionsaufrufe und reguläre Funktionen bereits syntaktisch: SomeFun()und some_fun(). Wenn wir in Elixir den Punkt entfernen, sind sie dieselben some_fun()und some_fun()weil Variablen denselben Bezeichner wie Funktionsnamen verwenden. Daher der Punkt.
José Valim

17

Ich weiß nicht, wie nützlich dies für andere sein wird, aber ich habe mich schließlich mit dem Konzept beschäftigt, um zu erkennen, dass Elixierfunktionen keine Funktionen sind.

Alles im Elixier ist ein Ausdruck. Damit

MyModule.my_function(foo) 

ist keine Funktion, sondern der Ausdruck, der durch Ausführen des Codes in zurückgegeben wird my_function. Es gibt eigentlich nur einen Weg, eine "Funktion" zu erhalten, die Sie als Argument weitergeben können, nämlich die anonyme Funktionsnotation zu verwenden.

Es ist verlockend, die fn- oder & -Notation als Funktionszeiger zu bezeichnen, aber es ist tatsächlich viel mehr. Es ist eine Schließung der Umgebung.

Wenn Sie sich fragen:

Benötige ich an dieser Stelle eine Ausführungsumgebung oder einen Datenwert?

Und wenn Sie die Ausführung mit fn benötigen, werden die meisten Schwierigkeiten viel deutlicher.


2
Es ist klar, dass dies MyModule.my_function(foo)ein Ausdruck ist, aber MyModule.my_function"könnte" ein Ausdruck gewesen sein, der eine Funktion "Objekt" zurückgibt. Aber da Sie die Arität erzählen müssen, würden Sie MyModule.my_function/1stattdessen so etwas brauchen . Und ich denke, dass sie entschieden haben, dass es besser ist, &MyModule.my_function(&1) stattdessen eine Syntax zu verwenden, die es ermöglicht, die Arität auszudrücken (und auch anderen Zwecken dient). Noch unklar, warum es einen ()Operator für benannte Funktionen und einen .()Operator für unbenannte Funktionen gibt
RubenLaguna

Ich denke, Sie wollen: &MyModule.my_function/1So können Sie es als Funktion weitergeben.
Jnmandal

14

Ich habe nie verstanden, warum Erklärungen dafür so kompliziert sind.

Es ist wirklich nur eine außergewöhnlich kleine Unterscheidung, kombiniert mit den Realitäten der "Funktionsausführung ohne Parens" im Ruby-Stil.

Vergleichen Sie:

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

Zu:

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

Während beide nur Bezeichner sind ...

  • fun1ist eine Kennung, die eine benannte Funktion beschreibt, die mit definiert ist def.
  • fun2 ist ein Bezeichner, der eine Variable beschreibt (die zufällig einen Verweis auf eine Funktion enthält).

Überlegen Sie, was das bedeutet, wenn Sie fun1oder fun2in einem anderen Ausdruck sehen? Rufen Sie bei der Auswertung dieses Ausdrucks die referenzierte Funktion auf oder verweisen Sie nur auf einen Wert aus dem Speicher?

Es gibt keine gute Möglichkeit, dies beim Kompilieren zu wissen. Ruby hat den Luxus, den Variablennamensraum zu überprüfen, um herauszufinden, ob eine Variablenbindung zu einem bestimmten Zeitpunkt eine Funktion beschattet hat. Elixir, das kompiliert wird, kann das nicht wirklich. Das macht die Punktnotation, sie sagt Elixir, dass sie eine Funktionsreferenz enthalten soll und dass sie aufgerufen werden soll.

Und das ist wirklich schwer. Stellen Sie sich vor, es gäbe keine Punktnotation. Betrachten Sie diesen Code:

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?

Angesichts des obigen Codes denke ich, dass es ziemlich klar ist, warum Sie Elixir den Hinweis geben müssen. Stellen Sie sich vor, jede Variablen-Referenzierung müsste nach einer Funktion suchen? Stellen Sie sich alternativ vor, welche Heldentaten notwendig wären, um immer darauf zu schließen, dass die variable Dereferenzierung eine Funktion verwendet?


12

Ich kann mich irren, da niemand es erwähnt hat, aber ich hatte auch den Eindruck, dass der Grund dafür auch das rubinrote Erbe ist, Funktionen ohne Klammern aufrufen zu können.

Arity ist offensichtlich involviert, aber lassen Sie es uns für eine Weile beiseite legen und Funktionen ohne Argumente verwenden. In einer Sprache wie Javascript, in der Klammern obligatorisch sind, ist es einfach, den Unterschied zwischen der Übergabe einer Funktion als Argument und dem Aufruf der Funktion zu unterscheiden. Sie rufen es nur auf, wenn Sie die Klammern verwenden.

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

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

Wie Sie sehen können, macht es keinen großen Unterschied, ob Sie es benennen oder nicht. Mit Elixier und Rubin können Sie jedoch Funktionen ohne Klammern aufrufen. Dies ist eine Designauswahl, die mir persönlich gefällt, aber sie hat den Nebeneffekt, dass Sie nicht nur den Namen ohne die Klammern verwenden können, da dies bedeuten könnte, dass Sie die Funktion aufrufen möchten. Dafür ist das da &. Wenn Sie arity appart für eine Sekunde verlassen, &bedeutet das Voranstellen Ihres Funktionsnamens , dass Sie diese Funktion explizit als Argument verwenden möchten und nicht, was diese Funktion zurückgibt.

Jetzt ist die anonyme Funktion etwas anders, da sie hauptsächlich als Argument verwendet wird. Auch dies ist eine Entwurfsentscheidung, aber das Rationale dahinter ist, dass sie hauptsächlich von Iteratorfunktionen verwendet wird, die Funktionen als Argumente verwenden. Sie müssen sie also offensichtlich nicht verwenden, &da sie standardmäßig bereits als Argumente betrachtet werden. Es ist ihr Zweck.

Das letzte Problem ist, dass Sie sie manchmal in Ihrem Code aufrufen müssen, weil sie nicht immer mit einer Iteratorfunktion verwendet werden, oder Sie codieren möglicherweise selbst einen Iterator. Da Ruby objektorientiert ist, bestand die Hauptmethode für die kleine Geschichte darin, die callMethode für das Objekt zu verwenden. Auf diese Weise können Sie das Verhalten der nicht obligatorischen Klammern konsistent halten.

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

Jetzt hat sich jemand eine Verknüpfung ausgedacht, die im Grunde wie eine Methode ohne Namen aussieht.

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

Auch dies ist eine Design-Wahl. Jetzt ist Elixier nicht objektorientiert und ruft daher sicher nicht die erste Form auf. Ich kann nicht für José sprechen, aber es sieht so aus, als ob die zweite Form in Elixier verwendet wurde, da sie immer noch wie ein Funktionsaufruf mit einem zusätzlichen Zeichen aussieht. Es ist nah genug an einem Funktionsaufruf.

Ich habe nicht über alle Vor- und Nachteile nachgedacht, aber es sieht so aus, als könnten Sie in beiden Sprachen nur mit den Klammern davonkommen, solange Sie Klammern für anonyme Funktionen obligatorisch machen. Es scheint so zu sein:

Obligatorische Klammern VS Etwas andere Notation

In beiden Fällen machen Sie eine Ausnahme, weil sich beide unterschiedlich verhalten. Da es einen Unterschied gibt, können Sie ihn genauso gut deutlich machen und sich für die andere Notation entscheiden. Die obligatorischen Klammern sehen in den meisten Fällen natürlich aus, sind aber sehr verwirrend, wenn die Dinge nicht wie geplant verlaufen.

Bitte schön. Dies ist möglicherweise nicht die beste Erklärung der Welt, da ich die meisten Details vereinfacht habe. Das meiste davon sind auch Designentscheidungen, und ich habe versucht, einen Grund dafür anzugeben, ohne sie zu beurteilen. Ich liebe Elixier, ich liebe Rubin, ich mag die Funktionsaufrufe ohne Klammern, aber wie Sie finde ich die Konsequenzen hin und wieder ziemlich irreführend.

Und in Elixier ist es nur dieser zusätzliche Punkt, während in Rubin Blöcke darüber liegen. Blöcke sind erstaunlich und ich bin überrascht, wie viel Sie mit nur Blöcken tun können, aber sie funktionieren nur, wenn Sie nur eine anonyme Funktion benötigen, die das letzte Argument ist. Da Sie dann in der Lage sein sollten, mit anderen Szenarien umzugehen, kommt hier die gesamte Methode / Lambda / Proc / Block-Verwirrung.

Wie auch immer ... das ist nicht möglich.


9

Es gibt einen ausgezeichneten Blog-Beitrag zu diesem Verhalten: Link

Zwei Arten von Funktionen

Wenn ein Modul Folgendes enthält:

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

Sie können dies nicht einfach ausschneiden und in die Shell einfügen, um das gleiche Ergebnis zu erzielen.

Es ist, weil es einen Fehler in Erlang gibt. Module in Erlang sind Sequenzen von FORMEN . Die Erlang-Shell wertet eine Folge von EXPRESSIONS aus . In Erlang sind FORMEN keine AUSDRÜCKE .

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

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

Die beiden sind nicht gleich. Dieses bisschen Albernheit war für immer Erlang, aber wir haben es nicht bemerkt und gelernt, damit zu leben.

Punkt beim Anrufen fn

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

In der Schule habe ich gelernt, Funktionen aufzurufen, indem ich f (10) und nicht f (10) geschrieben habe - dies ist „wirklich“ eine Funktion mit einem Namen wie Shell.f (10) (es ist eine in der Shell definierte Funktion). Der Shell-Teil ist implizit sollte es also einfach f (10) heißen.

Wenn Sie es so lassen, sollten Sie die nächsten zwanzig Jahre Ihres Lebens damit verbringen, zu erklären, warum.


Ich bin mir nicht sicher, ob es nützlich ist, die OP-Frage direkt zu beantworten, aber der bereitgestellte Link (einschließlich des Kommentarbereichs) ist eine großartige Lese-IMO für die Zielgruppe der Frage, dh diejenigen von uns, die Elixir + Erlang noch nicht kennen und lernen .

Der Link ist jetzt tot :(
AnilRedshift

2

Elixir verfügt optional über geschweifte Klammern für Funktionen, einschließlich Funktionen mit 0 arity. Sehen wir uns ein Beispiel an, warum eine separate Aufrufsyntax wichtig ist:

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

Ohne einen Unterschied zwischen zwei Arten von Funktionen zu machen, können wir nicht sagen, was Insanity.divebedeutet: eine Funktion selbst abrufen, aufrufen oder auch die resultierende anonyme Funktion aufrufen.


1

fn ->Die Syntax dient zur Verwendung anonymer Funktionen. Wenn Sie var. () Ausführen, wird Elixir nur gesagt, dass Sie diese var mit einer Funktion darin ausführen und ausführen sollen, anstatt die var als etwas zu bezeichnen, das nur diese Funktion enthält.

Elixir hat dieses gemeinsame Muster, bei dem anstelle einer Logik in einer Funktion, um zu sehen, wie etwas ausgeführt werden soll, verschiedene Funktionen basierend auf der Art der Eingabe, die wir haben, mit Mustern übereinstimmen. Ich nehme an, deshalb beziehen wir uns auf Dinge durch Arität im function_name/1Sinne.

Es ist etwas seltsam, sich an Kurzfunktionsdefinitionen (func (& 1) usw.) zu gewöhnen, aber praktisch, wenn Sie versuchen, Ihren Code zu leiten oder kurz zu halten.


0

Nur die zweite Art von Funktion scheint ein erstklassiges Objekt zu sein und kann als Parameter an andere Funktionen übergeben werden. Eine in einem Modul definierte Funktion muss in ein fn eingeschlossen werden. Es gibt etwas syntaktischen Zucker, der aussieht otherfunction(myfunction(&1, &2)), um das zu vereinfachen, aber warum ist er überhaupt notwendig? Warum können wir das nicht einfach tun otherfunction(myfunction))?

Du kannst tun otherfunction(&myfunction/2)

Da elixir Funktionen ohne die Klammern (wie myfunction) ausführen kann, otherfunction(myfunction))wird versucht, es auszuführen myfunction/0.

Sie müssen also den Erfassungsoperator verwenden und die Funktion einschließlich arity angeben, da Sie verschiedene Funktionen mit demselben Namen haben können. Also , &myfunction/2.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.