Una funzione è un codice riutilizzabile che fornisce istruzioni per l'esecuzione di un'azione, come ad esempio Dance() o Sleep(), e produce output diversi in base all'input fornito.
Le funzioni forniscono astrazione per i comportamenti; ciò vuol dire che, essendo riutilizzabili, nascondono i dettagli di implementazione che non sono rilevanti per altre parti del codice e che non è necessario visualizzare.
Utilizziamo l'ordinazione di cibo da un menu come esempio per comprendere il concetto di funzioni e astrazione. La funzione per ordinare cibo potrebbe assomigliare a quanto segue:
OrderFood(MenuItem : string) : food = {...}Quando ordini cibo in un ristorante, di solito comunichi al cameriere quale piatto del menu desideri,OrderFood("Ramen"). Non sai il modo in cui il ristorante preparerà il piatto, ma ti aspetti di ricevere qualcosa che è considerato un food dopo l'ordine. Altri clienti possono ordinare piatti diversi dal menu e anche aspettarsi di ricevere il loro cibo.
Per questo motivo le funzioni sono utili: è sufficiente definire queste istruzioni in un unico posto, in questo caso, definire cosa dovrebbe accadere quando qualcuno ordina da mangiare. Puoi quindi riutilizzare la funzione in contesti diversi, ad esempio per ogni cliente del ristorante che ordina dal menu.
Le sezioni seguenti descrivono come creare una funzione e come utilizzare una funzione una volta definita.
Definizione di funzioni
La firma di una funzione dichiara il nome della funzione (identificatore) e l'input (parametri), nonché l'output (risultato) della funzione.
In Verse si possono anche trovare gli specificatori, che indicano come utilizzare o implementare una funzione.
La funzione body è un blocco di codice che definisce cosa esegue la funzione, quando viene chiamata.
Le sezioni seguenti spiegano questi concetti in modo più dettagliato.
La sintassi della funzione è simile alla seguente:
Identifier(parameter1 : type, parameter2 : type) <specifier> : type = {}Parametri
Un parametro è una variabile di input dichiarata in una firma della funzione e viene utilizzato nel corpo della funzione. Quando si chiama una funzione, è necessario assegnare i valori ai parametri, se presenti. I valori assegnati sono chiamati argomenti della funzione.
Una funzione può non avere parametri, come nel caso di Sleep(), o avere tutti i parametri necessari. Puoi dichiarare un parametro nella firma della funzione specificando un identificatore e un type tra parentesi (). Se disponi di più parametri, devono essere separati da virgole ,.
Per esempio:
Example(Parameter1 : int, Parameter2 : string) : string = {}Quanto segue è tutto valido:
Foo():void = {}
Bar(X:int):int = X
Baz(X:int, ?Y:int, ?Z:int = 0) = X + Y + ZLa sintassi ?Y:int definisce un argomento denominato con il nome Y di tipo int.
La sintassi ?Z:int = 0 definisce un argomento denominato Z di tipo int che non deve essere fornito quando viene chiamata la funzione, ma utilizza 0 come valore, se non viene fornito.
Risultato
Il risultato è l'output di una funzione, quando viene chiamata. Il tipo return specifica il tipo di valore che ci si può aspettare dalla funzione, se viene eseguita correttamente.
Se non vuoi che la funzione abbia un risultato, puoi impostare il tipo restituito su void. Le funzioni con void come tipo restituito restituiscono sempre il valore false, anche quando si specifica un'espressione di risultato nel corpo della funzione.
Specificatori
Oltre agli specificatori sulla funzione che descrivono il comportamento della funzione definita, possono essere presenti specificatori sull'identificatore (il nome) della funzione. Per esempio:
Foo<public>(X:int)<decides>:int = X > 0In questo esempio viene definita una funzione denominata Foo, accessibile pubblicamente e con effetto decides. Gli specificatori dopo l'elenco dei parametri, e prima del tipo restituito, descrivono semantiche della funzione e contribuiscono a definire il tipo di funzione risultante. Gli specificatori sul nome della funzione indicano solo il comportamento correlato al nome della funzione definita, come ad esempio la sua visibilità.
Corpo della funzione
Il corpo della funzione corrisponde al blocco di codice che definisce cosa svolge la funzione. La funzione utilizza tutti i parametri definiti nella firma della funzione del corpo della funzione per creare un risultato.
Una funzione restituisce automaticamente il valore prodotto dall'ultima espressione eseguita.
Per esempio:
Foo()<decides>:void = {}
Bar():int =
if (Foo[]):
1
else:
2La funzione Bar() restituisce 1 o 2, in base all'esito negativo di Foo[].
Per forzare la restituzione di un determinato valore (e l'uscita immediata della funzione), utilizza l'espressione return.
Per esempio:
Minimum(X:int, Y:int):int =
if (X < Y):
return X
return YL'espressione return X uscirà dalla funzione Minimum, restituendo il valore contenuto in X al chiamante della funzione. Se non vengono utilizzate espressioni return esplicite, la funzione restituirà l'ultima espressione eseguita per impostazione predefinita. Ciò comporterebbe sempre la restituzione del valore Y, dando così un risultato potenzialmente errato.
Effetti
Gli effetti su una funzione descrivono i comportamenti aggiuntivi che possono essere assunti dalla funzione quando viene chiamata. In particolare, l'effetto decides su una funzione indica che la stessa funzione potrebbe avere esito negativo, in un modo che il chiamante potrebbe dover gestire (o propagarsi al suo chiamante essendo anche contrassegnato come decides).
Per esempio:
Fail()<decides>:void = false?Questo definisce una funzione che ha sempre esito negativo. Qualsiasi chiamante dovrebbe gestire o propagare l'errore. Nota la sintassi: l'effetto è descritto come uno specificatore sulla funzione. Il tipo di una funzione con tale effetto può essere reso molto simile alla definizione della funzione tramite la macro di tipo:
type{_()<decides>void}Le funzioni con effetto decides possono anche restituire un valore, se la funzione viene eseguita correttamente.
Per esempio:
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?Questa funzione determina il primo valore dell'array, che permette di avere un'esecuzione corretta della funzione decides fornita. Questa funzione utilizza un tipo option per mantenere il valore restituito e decidere se la funzione ha esito positivo o negativo, poiché non sono permesse istruzioni return esplicite all'interno di un contesto di errore.
Puoi combinare l'errore anche con le espressioni for. Una funzione decides con un'espressione di errore all'interno dell'espressione for viene eseguita correttamente solo se ogni iterazione dell'espressione for è eseguita a sua volta in modo corretto.
Per esempio:
All(Array:[]t, F(:t)<transacts><decides>:void where t:type)<transacts><decides>:void =
for (Element : Array):
F[Element]Questa funzione ha esito positivo solo se tutti gli elementi di Array sono eseguiti correttamente per la funzione F. Se un input di Array genera un errore per la funzione F, la funzione All genera a sua volta un errore.
Puoi anche combinare l'errore con un'espressione for per filtrare un input in base a quali input hanno esito positivo o negativo.
Per esempio:
Filter(Array:[]t, F(:t)<transacts><decides>:void where t:type)<transacts>:[]t =
for (Element : Array, F[Element]):
ElementQuesta funzione restituisce un array contenente solo gli elementi di Array che determinano l'esito positivo della funzione F.
Chiamata di funzioni
Una chiamata di funzione (nota anche come "chiamata" o "richiamo") è una espressione che valuta una funzione.
Ci sono due forme per le chiamate di funzione in Verse:
FunctionName(Arguments): questa forma richiede che la chiamata di funzione abbia esito positivo e possa essere utilizzata in qualsiasi contesto.FunctionName[Arguments]: questa forma indica che la chiamata di funzione può avere esito negativo. Per utilizzare questo modulo, la funzione deve essere definita con lo specificatore<decides>e chiamata in un contesto di errore.
Il richiamo viene eseguito utilizzando le parentesi, se la funzione non ha l'effetto decides. Per esempio:
Foo()
Bar(1)
Baz(1, ?Y := 2)
Baz(3, ?Y := 4, ?Z := 5)
Baz(6, ?Z := 7, ?Y := 8)Tieni presente come gli argomenti denominati, ad esempio ?Y:int, vengono passati facendo riferimento al nome anteposto da ? e fornendo un valore a destra di :=. Tieni presente inoltre che l'argomento denominato ?Z è facoltativo. È importante sottolineare che l'ordine degli argomenti denominati nel sito della chiamata è irrilevante, ad eccezione di eventuali effetti collaterali che possono verificarsi durante la produzione del valore per l'argomento denominato.
Per richiamare una funzione con l'effetto decides, è necessario utilizzare le parentesi quadre. Ciò consente all'indicizzazione dell'array, che comporta l'effetto decides, di seguire una sintassi simile alle funzioni contrassegnate con l'effetto decides. Per esempio:
Foo()<decides>:void = {}
Bar():int =
if (Foo[]):
1
else:
2Decompressione tupla
Una funzione che accetta più argomenti è indistinguibile quando viene richiamata da una funzione che accetta un singolo argomento tupla, dotata di elementi corrispondenti allo stesso tipo degli argomenti multipli. La mancanza di distinzione quando viene richiamata si applica anche al tipo di ciascuna funzione: hanno lo stesso tipo.
Per esempio:
Second(:any, X:t where t:type):t = XCiò equivale a:
Second(X:tuple(any, t) where t:type):t = X(1)Entrambi possono essere richiamati come:
X := 1
Y := 2
Second(X, Y)o
X:tuple(int, int) = (1, 2)
Second(X)Entrambi soddisfano il tipo type{_(:any, :t where t:type):t}.
Il tipo di funzione
Il tipo di una funzione è costituito dal tipo del suo parametro (potenzialmente definito come tupla decompressa), dal suo effetto e dal tipo del suo risultato. Per esempio:
type{_(:type1, :type2)<effect1>:type3}Si tratta del tipo di una funzione che accetta due argomenti di type1 e type2 (o un argomento di tipo tuple(type1, type2)); produce l'effetto effect1 e restituisce un valore di tipo type3.
Sovraccarico
Più funzioni possono condividere lo stesso nome purché non vi siano argomenti che soddisfino più di una di tali funzioni. Noto come sovraccarico.
Per esempio:
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?Non vi è alcuna sovrapposizione tra gli argomenti accettati da queste funzioni. La funzione corretta da richiamare può essere risolta in modo inequivocabile dai tipi forniti. Per esempio:
Next(0)
Next(0.0)
Next(int_list{Head := 0, Tail := int_list{Head := 1}})Tuttavia, non è consentito quanto segue:
First(X:int, :any):int = X
First(X:[]int)<decides>int = X[0]Questo perché tupla e array hanno una relazione di sottotipizzazione: array è un supertipo di tupla quando il tipo base dell'array è un supertipo di tutti i tipi di elemento della tupla. Per esempio:
X := (1, 2)
First(X)In questo esempio. la chiamata a First può essere soddisfatta da entrambe le definizioni First. Nel caso di classi e interfacce non può verificarsi alcun sovraccarico poiché una classe può essere modificata successivamente per implementare un'interfaccia o due classi possono essere modificate per avere una relazione di ereditarietà. È invece necessario utilizzare l'override del metodo. Per esempio,
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