Eine Funktion ist wiederverwendbarer Code, der Anweisungen für die Durchführung einer Aktion enthält, z.B. Dance() oder Sleep(), und der je nach Input unterschiedliche Outputs erzeugt.
Funktionen bieten Abstraktion für Verhaltensweisen, was bedeutet, dass diese wiederverwendbaren Funktionen die Implementierungsdetails verbergen, die für andere Teile Ihres Codes nicht relevant sind und die du nicht sehen musst.
Nehmen wir die Bestellung von Essen aus einer Speisekarte als Beispiel für Funktionen und Abstraktion. Die Funktion zum Bestellen von Essen könnte etwa so aussehen:
OrderFood(MenuItem : string) : food = {...}Wenn du in einem Restaurant Essen bestellst, sagst du dem Kellner, welches Gericht du auf der Speisekarte haben möchtest,OrderFood("Ramen"). Du weißt nicht, wie das Restaurant dein Gericht zubereiten wird, aber du erwartest, dass du nach der Bestellung etwas empfängst, das als food gilt. Andere Kunden können verschiedene Gerichte von der Speisekarte bestellen und erwarten ebenfalls, dass sie ihr Essen bekommen.
Deshalb sind Funktionen so nützlich: Du musst diese Anweisungen nur an einer Stelle definieren - in diesem Fall, um festzulegen, was passieren soll, wenn jemand Essen bestellt. Du kannst die Funktion dann in verschiedenen Kontexten wiederverwenden - zum Beispiel für jeden Kunden im Restaurant, der von der Speisekarte bestellt.
In den folgenden Abschnitten wird beschrieben, wie du eine Funktion erstellst und wie du eine Funktion verwendest, nachdem sie definiert wurde.
Funktionen definieren
Die Funktionssignatur deklariert den Funktionsnamen (Kennung), und die Input (Parameter) und das Output (Ergebnis) der Funktion.
Verse-Funktionen können auch Bezeichner haben, die angeben, wie eine Funktion verwendet oder implementiert werden soll.
Der Funktion Körper ist ein Code-Block, der definiert, was die Funktion tut, wenn sie aufgerufen wird.
In den folgenden Abschnitten werden diese Konzepte ausführlicher erläutert.
Die Funktionssyntax sieht folgendermaßen aus:
Identifier(parameter1 : type, parameter2 : type) <specifier> : type = {}Parameter
Ein Parameter ist eine Input-Variable, die in einer Funktionssignatur deklariert und im Körper der Funktion verwendet wird. Wenn du eine Funktion aufrufst, musst du den Parametern Werte zuweisen, falls es welche gibt. Die zugewiesenen Werte werden Argumente der Funktion genannt.
Eine Funktion kann keine Parameter haben - zum Beispiel Sleep() - oder so viele Parameter, die du brauchst. Du deklarierst einen Parameter in der Funktionssignatur, indem du einen Kennung und Typ zwischen den Klammern () angibst. Wenn du mehrere Parameter hast, müssen sie durch Kommas , getrennt werden.
Zum Beispiel:
Example(Parameter1 : int, Parameter2 : string) : string = {}Alle der folgenden Punkte sind gültig:
Foo():void = {}
Bar(X:int):int = X
Baz(X:int, ?Y:int, ?Z:int = 0) = X + Y + ZDie Syntax ?Y:int definiert ein benanntes Argument mit dem Namen Y vom Typ int.
Die Syntax ?Z:int = 0 definiert ein benanntes Argument mit dem Namen Z vom Typ int, das beim Aufruf der Funktion nicht angegeben werden muss, sondern 0 als Wert verwendet, wenn es nicht angegeben wird.
Ergebnis
Das Ergebnis ist der Output einer Funktion, wenn diese Funktion aufgerufen wird. Der Rückgabe-Typ gibt an, welche Art von Wert du von der Funktion erwarten kannst, wenn sie erfolgreich ausgeführt wird.
Wenn du nicht willst, dass deine Funktion ein Ergebnis hat, kannst du den Rückgabetyp auf void festlegen. Funktionen mit void als Rückgabe-Typ geben immer den Wert false zurück, auch wenn du einen Ergebnisausdruck im Funktionskörper angibst.
Spezifizierer
Zusätzlich zu den Bezeichnern der Funktion, die das Verhalten der definierten Funktion beschreiben, kann es auch Bezeichner für die Kennung (den Namen) der Funktion geben. Zum Beispiel:
Foo<public>(X:int)<decides>:int = X > 0In diesem Beispiel wird eine Funktion namens Foo definiert, die öffentlich zugänglich ist und den Effekt decides hat. Die Bezeichner nach der Parameterliste und vor dem Rückgabetyp beschreiben die Semantik der Funktion und tragen zum Typ der resultierenden Funktion bei. Angaben zum Namen der Funktion geben nur das Verhalten an, das mit dem Namen der definierten Funktion zusammenhängt, z. B. ihre Sichtbarkeit.
Funktionskörper
Der Funktionskörper ist der Code-Block, der definiert, was die Funktion tut. Die Funktion verwendet alle Parameter, die du in der Funktionssignatur des Funktionskörpers definierst, um ein Ergebnis zu erzeugen.
Eine Funktion gibt automatisch den Wert zurück, der durch den zuletzt ausgeführten Ausdruck erzeugt wurde.
Zum Beispiel:
Foo()<decides>:void = {}
Bar():int =
if (Foo[]):
1
else:
2Die Funktion Bar() gibt entweder 1 oder 2 zurück, je nachdem, ob Foo[] fehlschlägt.
Um die Rückgabe eines bestimmten Wertes (und das sofortige Beenden der Funktion) zu erzwingen, verwende den Ausdruck return.
Zum Beispiel:
Minimum(X:int, Y:int):int =
if (X < Y):
return X
return YDer Ausdruck return X beendet die Funktion Minimum und gibt den in X enthaltenen Wert an den Aufrufer der Funktion zurück. Beachte, dass die Funktion standardmäßig den zuletzt ausgeführten Ausdruck als Rückgabewert liefert, wenn hier keine expliziten return-Ausdrücke verwendet werden. Dies würde dazu führen, dass der Wert von Y immer zurückgegeben wird, was möglicherweise zu einem falschen Ergebnis führt.
Effekte
Effekte auf eine Funktion beschreiben zusätzliche Verhaltensweisen, die die Funktion beim Aufruf annehmen kann. Der Effekt decides bei einer Funktion zeigt insbesondere an, dass die Funktion auf eine Art und Weise fehlschlagen könnte, die der Aufrufer möglicherweise behandeln muss (oder an den Aufrufer weitergibt, indem er ebenfalls als decides markiert wird).
Zum Beispiel:
Fail()<decides>:void = false?Dies definiert eine Funktion, die immer fehlschlägt. Jeder Aufrufer müsste den Fehler behandeln oder weiterleiten. Beachte die Syntax: Der Effekt wird als Bezeichner für die Funktion beschrieben. Der Typ einer Funktion mit einer solchen Auswirkung kann über das Typ-Makro so gestaltet werden, dass er der Definition der Funktion sehr ähnlich ist:
type{_()<decides>void}Funktionen mit dem Effekt decides können auch einen Wert zurückgeben, sollte die Funktion erfolgreich sein.
Zum Beispiel:
First(Array:[]t, F(:t)<transacts><decides>:void where t:type)<transacts><decides>:t =
var ReturnOption:?t = false
for (Element : Array, F[Element], not ReturnOption?):
set ReturnOption = option{Element}
ReturnOption?Diese Funktion bestimmt den ersten Array-Wert, der zu einer erfolgreichen Ausführung der angegebenen decides-Funktion führt. Diese Funktion verwendet einen Typ option, um unseren Rückgabewert zu speichern und zu entscheiden, ob die Funktion erfolgreich ist oder fehlschlägt, da explizite return-Anweisungen innerhalb eines Fehlerkontexts nicht zulässig sind.
Du kannst Fehlschläge auch mit for-Ausdrücken kombinieren. Eine decides-Funktion mit einem Fehlerausdruck innerhalb des for-Ausdrucks ist nur dann erfolgreich, wenn jede Iteration des for-Ausdrucks erfolgreich ist.
Zum Beispiel:
All(Array:[]t, F(:t)<transacts><decides>:void where t:type)<transacts><decides>:void =
for (Element : Array):
F[Element]Diese Funktion ist nur dann erfolgreich, wenn alle Elemente von Array zu einem Erfolg für die Funktion F führen. Wenn irgendein Input von Array zu einem Fehler für die Funktion F führt, führt die Funktion All zu einem Fehler.
Du kannst auch Fehler mit einem for-Ausdruck kombinieren, um einen Input danach zu filtern, welche Eingaben zu Erfolg oder Misserfolg führen.
Zum Beispiel:
Filter(Array:[]t, F(:t)<transacts><decides>:void where t:type)<transacts>:[]t =
for (Element : Array, F[Element]):
ElementDiese Funktion gibt ein Array zurück, das nur die Elemente aus Array enthält, die zum Erfolg der Funktion F führen.
Funktionsaufruf
Ein Funktionsaufruf ist ein Ausdruck, der eine Funktion auswertet (bekannt als Aufruf).
Es gibt zwei Formen für Funktionsaufrufe in Verse:
FunctionName(Arguments): Diese Form setzt voraus, dass der Funktionsaufruf erfolgreich ist und kann in jedem Kontext verwendet werden.FunctionName[Arguments]: Diese Form bedeutet, dass der Funktionsaufruf fehlschlagen kann. Um diese Form zu verwenden, muss die Funktion mit dem Bezeichner<decides>definiert und in einem Fehlerkontext aufgerufen werden.
Der Aufruf erfolgt durch die Verwendung von Klammern, wenn die Funktion nicht den Effekt decides aufweist. Zum Beispiel:
Foo()
Bar(1)
Baz(1, ?Y := 2)
Baz(3, ?Y := 4, ?Z := 5)
Baz(6, ?Z := 7, ?Y := 8)Beachte, wie benannte Argumente, beispielsweise ?Y:int, übergeben werden, indem auf den vorangestellten Namen ? verwiesen und rechts von := ein Wert angegeben wird. Beachte auch, dass das benannte Argument ?Z optional ist. Wichtig ist, dass die Reihenfolge der benannten Argumente an der Aufrufstelle irrelevant ist, abgesehen von eventuellen Nebeneffekten, die beim Erzeugen des Wertes für das benannte Argument auftreten können.
Um eine Funktion aufzurufen, die den Effekt decides hat, sollten eckige Klammern verwendet werden. Dies ermöglicht es, dass die Indexierung von Arrays, die den decides-Effekt hat, einer ähnlichen Syntax folgt wie Funktionen, die mit dem decides-Effekt gekennzeichnet sind. Zum Beispiel:
Foo()<decides>:void = {}
Bar():int =
if (Foo[]):
1
else:
2Tupel auspacken
Eine Funktion, die mehrere Argumente akzeptiert, ist beim Aufruf nicht von einer Funktion zu unterscheiden, die ein einzelnes Tupel-Argument mit Elementen desselben Typs wie die mehreren Argumente akzeptiert. Die fehlende Unterscheidung beim Aufruf gilt auch für den Typ der einzelnen Funktionen - sie haben den gleichen Typ.
Zum Beispiel:
Second(:any, X:t where t:type):t = XDies ist gleichbedeutend mit:
Second(X:tuple(any, t) where t:type):t = X(1)Beide können so aufgerufen werden:
X := 1
Y := 2
Second(X, Y)oder
X:tuple(int, int) = (1, 2)
Second(X)Beide erfüllen den Typ type{_(:any, :t where t:type):t}.
Der Funktionstyp
Der Typ einer Funktion setzt sich aus dem Parametertyp (der möglicherweise als ungepacktes Tupel definiert ist), dem Effekt und dem Ergebnistyp zusammen. Zum Beispiel:
type{_(:type1, :type2)<effect1>:type3}Dies ist der Typ einer Funktion, die zwei Argumente vom Typ type1 und type2 (oder gleichwertig ein Argument vom Typ tuple(type1, type2)) entgegennimmt, den Effekt effect1 erzeugt und einen Wert vom Typ type3 zurückgibt.
Überlastung
Mehrere Funktionen können den gleichen Namen haben, solange es keine Argumente gibt, die mehr als eine solche Funktion erfüllen würden. Dies wird als Überlastung bezeichnet.
Zum Beispiel:
Next(X:int):int = X + 1
Next(X:float):float = X + 1
int_list := class:
Head:int
Tail:?int_list = false
Next(X:int_list)<decides>:int_list = X.Tail?Es gibt keine Überschneidungen bei den Argumenten, die eine dieser Funktionen akzeptiert. Die richtige Funktion, die aufgerufen werden soll, kann anhand der angegebenen Typen eindeutig bestimmt werden. Zum Beispiel:
Next(0)
Next(0.0)
Next(int_list{Head := 0, Tail := int_list{Head := 1}})Die folgenden Punkte sind jedoch nicht zulässig:
First(X:int, :any):int = X
First(X:[]int)<decides>int = X[0]Das liegt daran, dass Tupel und Array eine Subtyp-Beziehung haben: Zum Beispiel:
X := (1, 2)
First(X)In diesem Beispiel. Der Aufruf von First kann durch jede Definition von First erfüllt werden. Bei Klassen und Schnittstellen kann keine Überladung auftreten, da eine Klasse später geändert werden kann, um eine Schnittstelle zu implementieren, oder zwei Klassen geändert werden können, um eine Vererbungsbeziehung zu haben. Stattdessen sollte das Überschreiben von Methoden verwendet werden. Zum Beispiel
as_int := interface:
AsInt():int
ToInt(X:as_int):int = X.AsInt()
thing1 := class(as_int):
AsInt():int = 1
thing2 := class(as_int):
AsInt():int = 2