Une fonction est un code réutilisable qui fournit des instructions pour effectuer une action, comme Dance() ou Sleep(), et produit différents résultats en fonction des entrées que vous fournissez.
Les fonctions fournissent une abstraction pour les comportements, ce qui signifie que ces fonctions réutilisables cachent les détails d'implémentation qui ne sont pas pertinents pour d'autres parties de votre code, et que vous n'avez pas besoin de voir.
Prenons l'exemple de la commande de nourriture à partir d'un menu pour illustrer les fonctions et l'abstraction. La fonction pour commander de la nourriture pourrait ressembler à ceci :
OrderFood(MenuItem : string) : food = {...}Lorsque vous commandez de la nourriture dans un restaurant, vous indiquez au serveur le plat du menu que vous souhaitez : OrderFood("Ramen"). Vous ne savez pas comment le restaurant va préparer votre plat, mais vous vous attendez à recevoir quelque chose qui est considéré comme de la nourriture après avoir passé votre commande. Les autres clients peuvent commander différents plats du menu et s'attendre également à recevoir leur nourriture.
C'est pourquoi les fonctions sont utiles : vous n'avez besoin de définir ces instructions qu'à un seul endroit (en l'espèce, ce qui doit se passer lorsque quelqu'un commande de la nourriture). Vous pouvez ensuite réutiliser la fonction dans différents contextes, par exemple pour chaque client du restaurant qui commande un plat du menu.
Les sections ci-dessous décrivent comment créer une fonction et comment utiliser une fonction une fois qu'elle est définie.
Définir des fonctions
La signature de fonction déclare le nom de fonction (identificateur), ainsi que l'entrée (paramètres) et la sortie (résultat) de la fonction.
Les fonctions Verse peuvent également avoir des spécificateurs qui précisent la manière d'utiliser ou d'implémenter une fonction.
Le corps de la fonction est un bloc de code qui définit ce que fait la fonction lorsqu'elle est appelée.
Les sections ci-dessous expliquent ces concepts plus en détail.
La syntaxe de la fonction se présente comme suit :
Identifier(parameter1 : type, parameter2 : type) <specifier> : type = {}Paramètres
Un paramètre est une variable d'entrée déclarée dans une signature de fonction et utilisée dans le corps de la fonction. Lorsque vous appelez une fonction, vous devez attribuer des valeurs aux paramètres, s'il y en a. Les valeurs affectées sont appelées arguments de la fonction.
Une fonction peut n'avoir aucun paramètre (par ex. Sleep()), ou autant de paramètres que nécessaire. Vous déclarez un paramètre dans la signature de la fonction en spécifiant un identificateur et un type entre les parenthèses (). Si vous avez plusieurs paramètres, ils doivent être séparés par des virgules (,).
Par exemple :
Example(Parameter1 : int, Parameter2 : string) : string = {}Tous les éléments suivants sont valables :
Foo():void = {}
Bar(X:int):int = X
Baz(X:int, ?Y:int, ?Z:int = 0) = X + Y + ZLa syntaxe ?Y:int définit un argument nommé avec le nom Y de type int.
La syntaxe ?Z:int = 0 définit un argument nommé Z de type int qui n'a pas à être fourni lorsque la fonction est appelée, mais utilise 0 comme valeur s'il n'est pas fourni.
Résultat
Le résultat est la sortie d'une fonction qui est appelée. Le type de renvoi indique le type de valeur que vous pouvez attendre de la fonction si elle s'exécute correctement.
Si vous ne voulez pas que votre fonction ait un résultat, vous pouvez définir le type de renvoi sur void. Les fonctions dont le type de retour est void renvoient toujours la valeur false, même si vous spécifiez une expression de résultat dans le corps de la fonction.
Spécificateurs
En plus des spécificateurs sur la fonction qui décrivent le comportement de la fonction définie, il peut y avoir des spécificateurs sur l'identificateur (le nom) de la fonction. Par exemple :
Foo<public>(X:int)<decides>:int = X > 0Cet exemple définit une fonction nommée Foo qui est en accès libre et dispose de l'effet decides. Les spécificateurs situés après la liste des paramètres et avant le type de renvoi décrivent la sémantique de la fonction et contribuent au type de la fonction résultante. Les spécificateurs sur le nom de la fonction indiquent uniquement le comportement lié au nom de la fonction définie, comme sa visibilité.
Corps de la fonction
Le corps de la fonction est le bloc de code qui définit ce que fait la fonction. La fonction utilise tous les paramètres que vous définissez dans la signature de fonction du corps de la fonction pour créer un résultat.
Une fonction renvoie automatiquement la valeur produite par la dernière expression exécutée.
Par exemple :
Foo()<decides>:void = {}
Bar():int =
if (Foo[]):
1
else:
2La fonction Bar() renvoie soit 1 soit 2, selon que Foo[] échoue ou non.
Pour forcer le renvoi d'une valeur particulière (et la sortie immédiate de la fonction), utilisez l'expression return.
Par exemple :
Minimum(X:int, Y:int):int =
if (X < Y):
return X
return YL'expression return X quittera la fonction Minimum, en renvoyant la valeur contenue dans X à l'appelant de la fonction. Notez que si des expressions return explicites ne sont pas utilisées ici, la fonction renverra la dernière expression exécutée par défaut. Cela entraînerait le retour constant de la valeur de Y, ce qui peut produire un résultat incorrect.
Effets
Les effets d'une fonction décrivent les comportements supplémentaires que peut adopter la fonction lorsqu'elle est appelée. Plus précisément, l'effet decides sur une fonction indique qu'elle pourrait échouer d'une manière que l'appelant pourrait devoir gérer (ou propager à son appelant en étant également marquée comme decides).
Par exemple :
Fail()<decides>:void = false?Cela définit une fonction qui échoue toujours. Tout appelant devra gérer ou propager l'échec. Notez la syntaxe : l'effet est décrit comme un spécificateur sur la fonction. Le type d'une fonction avec un tel effet peut être rendu très proche de la définition de la fonction via la macro de type :
type{_()<decides>void}Les fonctions avec l'effet decides peuvent également renvoyer une valeur si la fonction réussit.
Par exemple :
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?Cette fonction détermine la première valeur de la matrice qui aboutit à une exécution réussie de la fonction decides fournie. Cette fonction utilise un type option pour contenir notre valeur de retour et décider si la fonction réussit ou échoue, car les instructions de retour explicites dans un contexte d'échec ne sont pas autorisées.
Vous pouvez également combiner l'échec avec des expressions for. Une fonction decides comprenant une expression d'échec à l'intérieur de l'expression for réussit uniquement si chaque itération de l'expression for réussit.
Par exemple :
All(Array:[]t, F(:t)<transacts><decides>:void where t:type)<transacts><decides>:void =
for (Element : Array):
F[Element]Cette fonction réussit uniquement si tous les éléments du résultat Array aboutissent à une réussite de la fonction F. Si une entrée de Array entraîne un échec de la fonction F, la fonction All aboutit également à un échec.
Vous pouvez également utiliser l'échec combiné avec une expression for pour filtrer une entrée en fonction des entrées qui aboutissent à une réussite ou à un échec.
Par exemple :
Filter(Array:[]t, F(:t)<transacts><decides>:void where t:type)<transacts>:[]t =
for (Element : Array, F[Element]):
ElementCette fonction renvoie une matrice contenant uniquement les éléments de Array qui entraînent la réussite de la fonction F.
Appeler des fonctions
Un appel de fonction est une expression qui évalue (c'est-à-dire qui appelle ou invoque) une fonction.
Il existe deux formes d'appels de fonctions dans Verse :
FunctionName(Arguments): cette forme exige que l'appel de fonction réussisse et puisse être utilisé dans n'importe quel contexte.FunctionName[Arguments]: cette forme signifie que l'appel de fonction peut échouer. Pour utiliser cette forme, la fonction doit être définie avec le spécificateur<decides>et appelée dans un contexte d'échec.
Si la fonction n'a pas l'effet decides, l'invocation est effectuée par l'utilisation de parenthèses. Par exemple :
Foo()
Bar(1)
Baz(1, ?Y := 2)
Baz(3, ?Y := 4, ?Z := 5)
Baz(6, ?Z := 7, ?Y := 8)Notez comment les arguments nommés, par exemple ?Y:int, sont passés en faisant référence au nom précédé de ? et en fournissant une valeur à droite de :=. Notez également que l'argument nommé ?Z est facultatif. Il est important de noter que l'ordre des arguments nommés au site d'appel n'est pas pertinent, sauf pour tout effet secondaire qui peut se produire lors de la production de la valeur de l'argument nommé.
Pour invoquer une fonction qui a l'effet decides, il faut utiliser des crochets. Cela permet à l'indexation de matrice, qui subit l'effet decides, de suivre une syntaxe semblable aux fonctions marquées par l'effet decides. Par exemple :
Foo()<decides>:void = {}
Bar():int =
if (Foo[]):
1
else:
2Décompression de tuple
Une fonction qui accepte plusieurs arguments est indiscernable lorsqu'elle est invoquée depuis une fonction qui accepte un seul argument tuple avec des éléments de même type que les arguments multiples. L'absence de distinction lors de l'invocation s'applique également au type de chaque fonction - elles ont le même type.
Par exemple :
Second(:any, X:t where t:type):t = XCela équivaut à :
Second(X:tuple(any, t) where t:type):t = X(1)Les deux peuvent être invoqués comme :
X := 1
Y := 2
Second(X, Y)ou
X:tuple(int, int) = (1, 2)
Second(X)Les deux satisfont le type type{_(:any, :t where t:type):t}.
Le type de fonction
Le type d'une fonction est constitué du type de son paramètre (potentiellement défini comme tuple décompressé), de son effet et du type de son résultat. Par exemple :
type{_(:type1, :type2)<effect1>:type3}Il s'agit du type d'une fonction qui utilise deux arguments de type1 et type2 (ou de manière équivalente, un argument de type tuple(type1, type2)), produit l'effet effect1 et renvoie une valeur de type type3.
Surcharge
Plusieurs fonctions peuvent partager le même nom, à condition qu'il n'y ait pas d'arguments qui satisfassent plus d'une de ces fonctions. C'est ce qu'on appelle la surcharge.
Par exemple :
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?Il n'y a pas de chevauchement dans les arguments que chacune de ces fonctions accepte. La fonction correcte à invoquer peut être résolue sans ambiguïté par les types fournis. Par exemple :
Next(0)
Next(0.0)
Next(int_list{Head := 0, Tail := int_list{Head := 1}})Toutefois, les éléments suivants ne sont pas admis :
First(X:int, :any):int = X
First(X:[]int)<decides>int = X[0]En effet, le tuple et la matrice ont une relation de sous-typage : la matrice est un super-type de tuple alors que le type de base de matrice est un super-type de tous les types d'éléments de tuple. Par exemple :
X := (1, 2)
First(X)Dans cet exemple, l'appel à First peut être satisfait par l'une des définitions de First. Dans le cas des classes et des interfaces, aucune surcharge ne peut se produire, car une classe peut être modifiée ultérieurement pour implémenter une interface ou deux classes peuvent être modifiées pour avoir une relation d'héritage. Au lieu de cela, il convient d'utiliser le remplacement des méthodes. Par exemple :
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