Funkcja to kod wielokrotnego użytku, który dostarcza instrukcje do wykonania akcji, takiej jak Dance() lub Sleep(), i generuje różne dane wyjściowe w oparciu o dostarczone dane wejściowe.
Funkcje zapewniają abstrakcję dla zachowań, co oznacza, że te funkcje wielokrotnego użytku ukrywają szczegóły implementacji, które nie są istotne dla innych części kodu i których nie musisz widzieć.
Jako przykład działania funkcji i abstrakcji wykorzystajmy zamawianie jedzenia z menu. Funkcja zamawiania jedzenia mogłaby wyglądać mniej więcej tak:
OrderFood(MenuItem : string) : food = {...}Zamawiając jedzenie w restauracji, mówisz kelnerowi, na które danie z menu masz ochotę: OrderFood("Ramen"). Nie wiesz, jak restauracja przygotuje zamówione danie, ale oczekujesz, że otrzymasz coś, co jest uważane za rodzaj food po złożeniu zamówienia. Pozostali klienci również mogą zamówić różne dania z menu i oczekiwać, że otrzymają jedzenie.
Właśnie dlatego funkcje okazują się przydatne – wystarczy, że zdefiniujesz te instrukcje w jednym miejscu – w tym przypadku definiując, co ma się stać, gdy ktoś zamówi jedzenie. Następnie funkcję tę można ponownie wykorzystać w różnych kontekstach – na przykład dla każdego klienta w restauracji, który zamawia z menu.
W poniższych sekcjach opisano, jak utworzyć funkcję i jak użyć funkcji po jej zdefiniowaniu.
Definiowanie funkcji
Sygnatura funkcji deklaruje nazwę funkcji (identyfikator), a także dane wejściowe (parametry) i dane wyjściowe (wynik) funkcji.
Funkcje Verse mogą również mieć specyfikatory, które określają sposób użycia lub implementacji funkcji.
Ciało funkcji jest blokiem kodu, który definiuje działanie funkcji.
W poniższych sekcjach wyjaśniono te pojęcia bardziej szczegółowo.
Składnia funkcji wygląda następująco:
Identifier(parameter1 : type, parameter2 : type) <specifier> : type = {}Parametry
Parametr jest zmienną wejściową zadeklarowaną w sygnaturze funkcji i używaną w treści funkcji. Wywołując funkcję, musisz przypisać wartości do parametrów, jeśli takie istnieją. Przypisane wartości nazywane są argumentami funkcji.
Funkcja może nie mieć żadnych parametrów – na przykład Sleep() – lub tyle parametrów, ile potrzebujesz. Deklarujesz parametr w sygnaturze funkcji, określając identyfikator i typ w nawiasach (). Jeśli masz wiele parametrów, należy je oddzielić przecinkami ,.
Na przykład:
Example(Parameter1 : int, Parameter2 : string) : string = {}Wszystkie poniższe przykłady są poprawne:
Foo():void = {}
Bar(X:int):int = X
Baz(X:int, ?Y:int, ?Z:int = 0) = X + Y + ZSkładnia ?Y:int definiuje nazwany argument o nazwie Y typu int.
Składnia ?Z:int = 0 definiuje nazwany argument o nazwie Z typu int, którego podanie nie jest wymagane, gdy funkcja jest wywoływana, ale używa 0 jako swojej wartości, jeśli nie zostanie podany.
Rezultat
Wynik jest efektem wyjściowym funkcji, gdy ta funkcja jest wywoływana. Typ return określa, jakiego typu wartości można oczekiwać od funkcji, jeśli zostanie ona pomyślnie wykonana.
Jeśli nie chcesz, aby funkcja zwracała wynik, możesz ustawić typ zwracanego wyniku na void. Funkcje z void jako typem zwracanym zawsze zwracają wartość false, nawet jeśli określisz wyrażenie wyniku w treści funkcji.
Specyfikatory
Oprócz specyfikatorów funkcji, które opisują zachowanie zdefiniowanej funkcji, mogą istnieć specyfikatory identyfikatora (nazwy) funkcji. Na przykład:
Foo<public>(X:int)<decides>:int = X > 0W tym przykładzie zdefiniowano funkcję o nazwie Foo, która jest publicznie dostępna i ma efekt decides. Specyfikatory po liście parametrów i przed typem zwracanym opisują semantykę funkcji i wpływają na typ funkcji wynikowej. Specyfikatory nazwy funkcji wskazują jedynie zachowanie związane z nazwą zdefiniowanej funkcji, takie jak jej widoczność.
Ciało funkcji
Ciało funkcji to blok kodu, który definiuje działanie funkcji. Funkcja wykorzystuje wszelkie parametry, które zostały zdefiniowane w sygnaturze ciała funkcji, aby utworzyć wynik.
Funkcja automatycznie zwraca wartość wygenerowaną przez ostatnio wykonane wyrażenie.
Na przykład:
Foo()<decides>:void = {}
Bar():int =
if (Foo[]):
1
else:
2Funkcja Bar() zwraca wartość 1 lub 2, w zależności od tego, czy Foo[] zakończy się niepowodzeniem.
Aby wymusić zwrócenie określonej wartości (i natychmiastowe wyjście z funkcji), należy użyć wyrażenia return.
Na przykład:
Minimum(X:int, Y:int):int =
if (X < Y):
return X
return YWyrażenie return X spowoduje wyjście z funkcji Minimum, zwracając wartość zawartą w X do wywołującego funkcję. Zwróć uwagę, że jeśli nie zastosuje się w tym miejscu wyrażeń return, funkcja domyślnie zwróci ostatnio wykonane wyrażenie. W ten sposób wartość Y byłaby zawsze zwracana, potencjalnie dając niepoprawny wynik.
Jakość efektów
Efekty funkcji opisują dodatkowe zachowania, które funkcja może podjąć po wywołaniu. Ściślej mówiąc, efekt decides dla funkcji wskazuje, że funkcja może zawieść w sposób, który może być obsługiwany przez wywoływacz (lub propagowany do wywoływacza poprzez oznaczenie jako decides).
Na przykład:
Fail()<decides>:void = false?Definiuje to funkcję, która zawsze kończy się niepowodzeniem. Każdy wywoływacz musiałby obsłużyć lub propagować niepowodzenie. Zwróć uwagę na składnię: efekt jest opisany jako specyfikator funkcji. Typ funkcji z takim efektem może być bardzo zbliżony do definicji funkcji za pomocą makra typu:
type{_()<decides>void}Funkcje z efektem decides mogą również zwrócić wartość, jeśli funkcja zakończy się powodzeniem.
Na przykład:
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?Funkcja ta określa pierwszą wartość tablicy, która powoduje pomyślne wykonanie podanej funkcji decides. Ta funkcja wykorzystuje typ option do przechowywania wartości zwracanej i decyduje o tym, czy funkcja się powiedzie, ponieważ jawne instrukcje return z kontekstu niepowodzenia są niedozwolone.
Niepowodzenie można także łączyć z wyrażeniami for. Funkcja decides, że funkcja z wyrażeniem zawodnym wewnątrz wyrażenia for kończy się powodzeniem tylko wtedy, gdy każda iteracja wyrażenia for zakończy się powodzeniem.
Na przykład:
All(Array:[]t, F(:t)<transacts><decides>:void where t:type)<transacts><decides>:void =
for (Element : Array):
F[Element]Ta funkcja powiedzie się tylko wtedy, gdy wszystkie elementy Array spowodują powodzenie funkcji F. Jeśli jakiekolwiek dane wejściowe z Array spowodują niepowodzenie funkcji F, funkcja All zwróci niepowodzenie.
Niepowodzenie można wykorzystać również w połączeniu z wyrażeniem for, aby przefiltrować dane wejściowe na podstawie tego, które z nich prowadzą do powodzenia, a które nie.
Na przykład:
Filter(Array:[]t, F(:t)<transacts><decides>:void where t:type)<transacts>:[]t =
for (Element : Array, F[Element]):
ElementFunkcja ta zwraca tablicę zawierającą tylko te elementy z tablicy Array, które powodują powodzenie funkcji F.
Wywoływanie funkcji
Wywołanie funkcji to wyrażenie, które ocenia (wywołuje lub przywołuje) funkcję.
W Verse dostępne są dwie formy wywoływania funkcji:
FunctionName(Arguments): Ta forma wymaga, aby wywołanie funkcji powiodło się, i może być używana w dowolnym kontekście.FunctionName[Arguments]: Ta forma oznacza, że wywołanie funkcji może zakończyć się niepowodzeniem. Aby użyć tej formy, funkcję należy zdefiniować ze specyfikatorem<decides>i wywołać w kontekście niepowodzenia.
Przywołanie wykonuje się za pomocą nawiasów, jeśli funkcja nie ma efektu decides. Na przykład:
Foo()
Bar(1)
Baz(1, ?Y := 2)
Baz(3, ?Y := 4, ?Z := 5)
Baz(6, ?Z := 7, ?Y := 8)Zwróć uwagę, jak nazwane argumenty, na przykład ?Y:int, są przekazywane poprzez odwołanie się do nazwy poprzedzonej ? i podanie wartości po prawej stronie :=. Zwróć również uwagę, że nazwany argument ?Z jest opcjonalny. Co ważne, kolejność nazwanych argumentów w miejscu wywołania nie ma znaczenia, z wyjątkiem wszelkich efektów ubocznych, które mogą wystąpić podczas tworzenia wartości dla nazwanego argumentu.
Aby przywołać funkcję, która ma efekt decides, należy użyć nawiasów kwadratowych. Pozwala to na indeksowanie tablicy, co powoduje efekt decides i uzyskanie składni podobnej do funkcji oznaczonych efektem decides. Na przykład:
Foo()<decides>:void = {}
Bar():int =
if (Foo[]):
1
else:
2Rozpakowywanie krotki
Funkcja, która przyjmuje wiele argumentów, jest nie do odróżnienia od funkcji, która jest wywoływana z funkcji, która przyjmuje pojedynczy argument krotki z elementami tych samych typów, co wiele argumentów. Nierozróżnianie po przywołaniu dotyczy również typu każdej funkcji – mają one ten sam typ.
Na przykład:
Second(:any, X:t where t:type):t = XTen przykład jest odpowiednikiem następującego zapisu:
Second(X:tuple(any, t) where t:type):t = X(1)Oba mogą zostać przywołane jako:
X := 1
Y := 2
Second(X, Y)lub
X:tuple(int, int) = (1, 2)
Second(X)Oba są zgodne z typem type{_(:any, :t where t:type):t}.
Typ funkcji
Typ funkcji składa się z jej typu parametru (potencjalnie zdefiniowanego jako rozpakowana krotka), jej efektu i typu wyniku. Na przykład:
type{_(:type1, :type2)<effect1>:type3}Jest to typ funkcji, która przyjmuje dwa argumenty typu type1 i type2 (lub równoważnie, jeden argument typu tuple(type1, type2)), tworzy efekt effect1 i zwraca wartość typu type3.
Przeciążenie
Wiele funkcji może mieć tę samą nazwę, o ile nie ma argumentów, które odpowiadałyby więcej niż jednej takiej funkcji. Zjawisko to jest nazywane przeciążeniem.
Na przykład:
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?Argumenty akceptowane przez każdą z tych funkcji nie pokrywają się. Prawidłowa funkcja do przywołania może być jednoznacznie określona przez podane typy. Na przykład:
Next(0)
Next(0.0)
Next(int_list{Head := 0, Tail := int_list{Head := 1}})Następujące elementy nie są jednak dozwolone:
First(X:int, :any):int = X
First(X:[]int)<decides>int = X[0]Dzieje się tak, ponieważ krotka i tablica są ze sobą powiązane relacją podtypowania: tablica jest nadtypem krotki, gdy typ bazowy tablicy jest nadtypem wszystkich typów elementów krotki. Na przykład:
X := (1, 2)
First(X)W tym przykładzie. wywołanie First może być spełnione przez dowolną definicję First. W przypadku klas i interfejsów nie może wystąpić przeciążenie, ponieważ klasę można później zmodyfikować, aby zaimplementować interfejs, lub dwie klasy można zmienić tak, aby miały relację dziedziczenia. Zamiast tego należy użyć zastępowania metod. Przykładowo
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