Функция — это многократно используемый код, который содержит инструкции для выполнения действия, такого как 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
.
Результат
Результат — это выходные данные функции при её вызове. Возвращаемый тип определяет, какого типа значение можно ожидать от функции в случае её успешного выполнения.
Если вы не хотите, чтобы функция выдавала результат, можно установить возвращаемый тип 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
.
Пример:
Find(X:[]int, F(:int)<decides>:void)<decides>:int =
for (Y:X, F(Y)):
return Y
false?
Выражение return Y
приводит к выходу из функции Find
, возвращая вызывающей функции значение, содержащееся в Y
. Обратите внимание, что false?
используется для принудительного завершения функции с неоднозначным результатом. В данном случае это имеет смысл, поскольку в X
не было найдено значения, соответствующего предикату F
. Если для функции определён возвращаемый тип void
, выражение return
не требуется.
Пример:
AnyOf(X:[]int, F(:int)<decides>:void)<decides>:void =
for (Y:X, F(Y)):
return
false?
Эффекты
Эффекты функции описывают её дополнительное поведение при вызове. В частности, эффект decides
указывает на то, что эта функция может завершиться с неоднозначным результатом, так что вызывающему коду может потребоваться обработчик (или этот эффект распространится на вызывающий код, который также помечается как decides
).
Пример:
Fail()<decides>:void = false?
Здесь определена функция, которая всегда завершается с неопределённым результатом. Любой вызывающий код должен будет обработать или передать неоднозначность дальше. Обратите внимание на синтаксис: эффект описывается как спецификатор функции. Тип функции с таким эффектом можно сделать очень похожим на определение функции через макрос type
:
type{_()<decides>void}
Вызов функции
Вызов функции — это выражение, которое оценивает функцию (также говорят: «вызывает» функцию или «обращается» к ней).
В 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, где 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
Main()<decides>:void =
X := thing1{}
ToInt(X) = 1
Y := thing2{}
ToInt(Y) = 2
Функция, являющаяся частью определения класса, называется методом. Она обладает дополнительными возможностями. После ознакомления с функциями в Verse прочитайте страницу «Класс», чтобы узнать больше о методах.