Функция — это многократно используемый код, который содержит инструкции для выполнения действия, такого как Dance() или Sleep(), и который выдаёт различные выходные данные с учётом входных данных.
Функции позволяют создавать абстракции поведения. Это значит, что многократно используемые функции скрывают детали реализации, которые не имеют отношения к другим частям кода и которые можно не показывать.
В качестве примера использования функций и абстракций рассмотрим заказ еды из меню. Такая функция может выглядеть следующим образом:
OrderFood(MenuItem : string) : food = {...}Когда вы заказываете еду в ресторане, то говорите официанту, какое блюдо из меню хотите: OrderFood("Рамен"). Вы не знаете, как ресторан приготовит это блюдо, но ожидаете, что после заказа получите что-то, что считается едой (food). Другие клиенты могут заказывать различные блюда из меню и также ожидать, что получат свою еду.
Именно поэтому функции полезны — достаточно определить в одном месте необходимые инструкции. В данном случае указать, что должно произойти, когда кто-то заказывает еду. Затем можно повторно использовать функцию в различных контекстах, например, когда клиенты заказывают блюда из меню в ресторане.
В следующих разделах мы покажем, как создать функцию и как использовать функцию после её определения.
Определение функций
Сигнатура функции объявляет имя функции (идентификатор), а также входные данные (параметры) и выходные данные (результат) функции.
Функции Verse также могут иметь спецификаторы, которые указывают, как использовать или реализовывать функцию.
Тело функции — это блок кода, который определяет, что делает функция при вызове.
Мы поговорим подробнее об этих понятиях в следующих разделах.
Синтаксис функции выглядит следующим образом:
Identifier(parameter1 : type, parameter2 : type) <specifier> : type = {}Параметры
Параметр — это входная переменная, объявленная в сигнатуре функции и используемая в её теле. При вызове функции необходимо присвоить значения параметрам, если они есть. Присвоенные значения называются аргументами функции.
Функция может не иметь параметров — например, Sleep(), — или иметь столько параметров, сколько нужно. Чтобы объявить параметр в сигнатуре функции, нужно указать идентификатор и тип внутри круглых скобок (). Если у вас несколько параметров, они должны быть разделены запятыми ,.
Пример:
Example(Parameter1 : int, Parameter2 : string) : string = {}Допустимы все следующие варианты:
Foo():void = {}
Bar(X:int):int = X
Baz(X:int, ?Y:int, ?Z:int = 0) = X + Y + ZСинтаксис ?Y:int определяет именованный аргумент типа int с именем Y.
Синтаксис ?Z:int = 0 определяет именованный аргумент типа int с именем Z, который необязательно задавать при вызове функции. Если этот аргумент не задан, то в качестве его значения используется 0.
Результат
Результат — это выходные данные функции при её вызове. Тип return определяет, какого типа значение можно ожидать от функции в случае её успешного выполнения.
Если вы не хотите, чтобы функция выдавала результат, можно установить возвращаемый тип void. Функции с возвращаемым типом void всегда возвращают значение false, даже если вы укажете выражение result в теле функции.
Спецификаторы
В дополнение к спецификаторам функции, описывающим поведение определяемой функции, можно задать спецификаторы для идентификатора (имени) функции. Пример:
Foo<public>(X:int)<decides>:int = X > 0В этом примере определяется функция Foo, которая находится в открытом доступе и имеет эффект decides. Спецификаторы после списка параметров и перед возвращаемым типом описывают семантику функции и определяют тип результирующей функции. Спецификаторы имени функции изменяют только поведение, связанное с именем ранее определённой функции, например её видимость.
Тело функции
Тело функции — это блок кода, который определяет, что делает функция. Результат функции зависит от всех параметров, которые определены в сигнатуре её тела.
Функция автоматически возвращает значение, полученное в последнем выполненном выражении.
Пример:
Foo()<decides>:void = {}
Bar():int =
if (Foo[]):
1
else:
2Функция Bar() возвращает либо 1, либо 2, в зависимости от того, удалось ли выполнить Foo[].
Чтобы принудительно вернуть конкретное значение (и немедленно выйти из функции), используйте выражение return.
Пример:
Minimum(X:int, Y:int):int =
if (X < Y):
return X
return YВыражение return X приводит к выходу из функции Minimum, возвращая вызывающей функции значение, содержащееся в X. Обратите внимание, что если явные выражения return не используются, функция по умолчанию вернёт последнее выполненное выражение. В итоге будет всегда возвращаться значение Y, что может давать неверный результат.
Эффекты
Эффекты функции описывают её дополнительное поведение при вызове. В частности, эффект decides в функции указывает на то, что функция может завершиться с неоднозначным результатом, так что вызывающему коду может потребоваться обработчик (или этот эффект распространится на вызывающий код, который также помечается как decides).
Пример:
Fail()<decides>:void = false?Здесь определена функция, которая всегда завершается с неопределённым результатом. Любой вызывающий код должен будет обработать или передать неоднозначность дальше. Обратите внимание на синтаксис: эффект описывается как спецификатор функции. Тип функции с таким эффектом можно сделать очень похожим на определение функции через макрос `type`:
type{_()<decides>void}Функции с эффектом decides также могут вернуть значение, если выполнятся успешно.
Пример:
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?Эта функция определяет первое значение массива, которое приводит к успешному выполнению предоставленной функции decides. Эта функция использует тип option для хранения возвращаемого значения и принятия решения о том, завершится ли функция успешно или безрезультатно, поскольку явные инструкции return в контексте, допускающем неоднозначность, недопустимы.
Также отсутствие результата можно сочетать с выражениями for. Функция decides с выражением с возможным отсутствием результата внутри выражения for выполняется успешно только в том случае, если каждая итерация выражения for завершается успешно.
Пример:
All(Array:[]t, F(:t)<transacts><decides>:void where t:type)<transacts><decides>:void =
for (Element : Array):
F[Element]Эта функция выполняется успешно только в том случае, если все элементы массива Array приводят к успешному выполнению функции F. Если какие-либо входные данные из массива Array приводят к невыполнению функции F, то функция All также не выполняется.
Вы также можете сочетать отсутствие результата с выражением for, чтобы фильтровать входные данные на основе того, какие из них приводят к успеху, а какие — к отсутствию результата.
Пример:
Filter(Array:[]t, F(:t)<transacts><decides>:void where t:type)<transacts>:[]t =
for (Element : Array, F[Element]):
ElementЭта функция возвращает массив, содержащий только элементы из массива Array, что приводит к успешному выполнению функции F.
Вызов функций
Вызов функции — это выражение, которое оценивает функцию (также говорят: «вызывает» функцию или «обращается» к ней).
В Verse есть две формы вызова функций:
FunctionName(Arguments): она может использоваться в любом контексте.FunctionName[Arguments]: эта форма означает, что вызов функции имеет неоднозначность. Для этой формы требуется спецификатор<decides>в определении функции, а также контекст, допускающий неоднозначность.
Обращение осуществляется с помощью круглых скобок, если функция не имеет эффекта decides. Пример:
Foo()
Bar(1)
Baz(1, ?Y := 2)
Baz(3, ?Y := 4, ?Z := 5)
Baz(6, ?Z := 7, ?Y := 8)Обратите внимание, что именованные аргументы, например ?Y:int, передаются ссылкой на имя, которое предваряется знаком ?, а значение приводится справа от :=. Также обратите внимание, что именованный аргумент ?Z является необязательным. Важно отметить, что порядок именованных аргументов в месте вызова не имеет значения, за исключением любого побочного эффекта, который может возникнуть при получении значения именованного аргумента.
Чтобы обратиться к функции, которая имеет эффект decides, следует использовать квадратные скобки. Таким образом, для индексирования массива, который принимает на себя результаты эффекта decides, используется синтаксис, аналогичный синтаксису функций, помеченных эффектом decides. Пример:
Foo()<decides>:void = {}
Bar():int =
if (Foo[]):
1
else:
2Распаковка кортежа
Вызов функции с несколькими аргументами идентичен вызову функции, у которой в качестве аргумента используется один кортеж элементов одного типа. При вызове также нет различий между типами функций: для всех них применяется общий тип.
Пример:
Second(:any, X:t where t:type):t = XЭтот код эквивалентен следующему:
Second(X:tuple(any, t) where t:type):t = X(1)Обе функции можно вызывать так:
X := 1
Y := 2
Second(X, Y)или
X:tuple(int, int) = (1, 2)
Second(X)Обе соответствуют типу type{_(:any, :t where t:type):t}.
Тип функции
Тип функции определяется типом её параметров (может быть определён как распакованный кортеж), её эффектом и типом её результата. Пример:
type{_(:type1, :type2)<effect1>:type3}Это тип функции, которая принимает два аргумента type1 и type2 (или, что эквивалентно, один аргумент типа tuple(type1, type2)), производит эффект effect1 и возвращает значение типа type3.
Перегрузка
Несколько функций могут иметь одно и то же имя при условии, что у них нет общих аргументов. Это явление называется перегрузкой.
Пример:
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?Аргументы, принимаемые функцией, не пересекаются. При вызове нужная функция однозначно определяется типом подставленных аргументов. Пример:
Next(0)
Next(0.0)
Next(int_list{Head := 0, Tail := int_list{Head := 1}})Однако следующий код недопустим:
First(X:int, :any):int = X
First(X:[]int)<decides>int = X[0]Это связано с тем, что у кортежа и массива есть отношение подтипирования: массив является супертипом кортежа, а базовый тип массива является супертипом всех типов элементов кортежа. Пример:
X := (1, 2)
First(X)В этом примере вызов First удовлетворяет любое определение First. У классов и интерфейсов не может быть перегрузки, поскольку классы можно изменить, реализовав интерфейс или отношение наследования между двумя классами. Вместо этого следует использовать переопределение метода. Так,
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