Wiki

Clone wiki

MindStream / Статьи на русском / Устройство скриптовой машины / PROCEDURE, FUNCTION. Параметры справа и слева. Часть 1

По мотивам

Предыдущая серия была тут - О кешировании. Поговорим про добавление вложенных элементов.

Ну и тут поступил вопрос про параметры слов (функций).

Раз есть вопрос, то постараюсь описать - как определяются слова и их параметры.

Историю коммитов можно посмотреть тут.

Ещё раз оговорюсь:

Идеологически наша скриптовая машина построена на стековой FORTH-машине.

Так что - было бы неплохо ознакомится с языком FORTH.

Для "общего понимания".

Итак.

Простейшее слово определяется так:

#!delphi
: // - признак начала слова
A // - имя слова
 1 >>std::out // - код слова - печатает число 1 или
 1 . // - также печатает число 1
; // - признак конца слова

A // - вызов слова A

Пример можно скопировать в файл Example.script и запустить:

call.ms.script.exe Example.script или call.ms.script.exe Example.script > a.out

Утилита call.ms.script.exe находится тут.

Замечение: Утилита может молча не запускаться, это значит, что её блокирует антивирус. Так как она "получена не из благонадёжного источника".

В этом случае её стоит проверить антивирусом и включить в список разрешённых программ. Продолжим.

Естественно, как и в любом другом языке программирования, у нас слова могут иметь параметры.

Простейший пример:

#!delphi
: A
  IN aParam // - определяем параметр aParam, слева от нашего слова A
 aParam // - получаем значение параметра
 . // - печатаем значение параметра
: // A

1 A // - вызов нашего слова A с передачей ему ЗНАЧЕНИЯ числа 1 как значения параметра

Можно расширить пример и определить ТИП параметра. Вот так:

#!delphi
: A
  INTEGER IN aParam // - определяем ЦЕЛОЧИСЛЕННЫЙ параметр aParam, слева от нашего слова A
 aParam // - получаем значение параметра
 . // - печатаем значение параметра
: // A

1 A // - вызов нашего слова A с передачей ему ЗНАЧЕНИЯ числа 1 как значения параметра
Тогда в нашу функцию можно будет передать ТОЛЬКО ЦЕЛОЧИСЛЕННЫЕ значения.

Но о типах параметров и переменных мы поговорим чуть позже. В отдельной статье.

В качестве "лирического отступления" рекомендую ознакомиться с описанием "базовой аксиоматики".

До сих пор мы рассмотрели ОДИН параметр слева.

Рассмотрим теперь НЕСКОЛЬКО параметров слева.

Пример:

#!delphi
: A
  INTEGER IN aParam1 // - определяем первый параметр
  INTEGER IN aParam2 // - определяем второй параметр
 aParam1 // - получаем значение параметра aParam1
 aParam2 // - получаем значение параметра aParam2
 + // - получаем сумму двух значений
 . // - печатаем получившийя результат
; // A

1 2 A // - вызываем наше слово A для двух ЦЕЛОЧИСЛЕННЫХ значений - 1 и 2

Хорошо. Мы поговорили про передачу параметров в слово.

А как получить значение из слова?

Разберём этот вопрос.

Простейший пример:

#!delphi
: A
  INTEGER IN aParam1 // - определяем первый параметр
  INTEGER IN aParam2 // - определяем второй параметр
 aParam1 // - получаем значение параметра aParam1
 aParam2 // - получаем значение параметра aParam2
 + // - получаем сумму двух значений
 // - тут ничего не печатаем, а просто оставляем полученное значение на стеке
; // A

1 2 A // - вызываем наше слово A для двух ЦЕЛОЧИСЛЕННЫХ значений - 1 и 2
. // - печатаем значение со стека, фактически то, что нам вернула функция A
Методика "оставления значения на стеке" используется не только в "допотопном FORTH", но и во вполне себе "современном Ruby".

Тут есть один недостаток (он же на самом деле - преимущество) - вызываемая функция может не положить на стек НИЧЕГО, может положить ОДНО значение, а может положить НЕСКОЛЬКО значений.

А вызывающая сторона этот факт не сможет проконтролировать.

Как быть?

Для этого нам надо определить ТИП ВОЗВРАЩАЕМОГО значения.

Пример:

#!delphi
INTEGER // - определяем тип возвращаемого значения и "неявную переменную" Result
: A
  INTEGER IN aParam1 // - определяем первый параметр
  INTEGER IN aParam2 // - определяем второй параметр
 aParam1 // - получаем значение параметра aParam1
 aParam2 // - получаем значение параметра aParam2
 + // - получаем сумму двух значений
 >>> Result // - снимаем значение со стека и кладём его в переменную Result.
; // A

1 2 A // - вызываем наше слово A для двух ЦЕЛОЧИСЛЕННЫХ значений - 1 и 2
. // - печатаем значение со стека, фактически то, что нам вернула функция A
В данном случае, скриптовая машина ГАРАНТИРУЕТ, что будет возвращено ОДНО целочисленное значение. ОДНО и ТОЛЬКО ОДНО.

Но "есть одно но".

Возврат - скриптовая машина - контроллирует, а вот забор со стека - нет.

И можно написать так.

#!delphi
INTEGER // - определяем тип возвращаемого значения и "неявную переменную" Result
: A
  INTEGER IN aParam1 // - определяем первый параметр
  INTEGER IN aParam2 // - определяем второй параметр
 aParam1 // - получаем значение параметра aParam1
 aParam2 // - получаем значение параметра aParam2
 + // - получаем сумму двух значений aParam1 и aParam2
 + // - получаем сумму предыдущего значения и того, что лежало на стеке
 >>> Result // - снимаем значение со стека и кладём его в переменную Result.
; // A

1 2 3 A // - вызываем наше слово A для ТРЁХ значений - 1, 2 и 3
Это иногда полезно, но как быть когда хочется "полного контроля"?

Для этого есть "аналоги" слова : - FUNCTION и PROCEDURE.

Пример:

#!delphi
INTEGER // - определяем тип возвращаемого значения и "неявную переменную" Result
FUNCTION // - используем FUNCTION вместо :
 A
  INTEGER IN aParam1 // - определяем первый параметр
  INTEGER IN aParam2 // - определяем второй параметр
 aParam1 // - получаем значение параметра aParam1
 aParam2 // - получаем значение параметра aParam2
 + // - получаем сумму двух значений
 >>> Result // - снимаем значение со стека и кладём его в переменную Result.
; // A

1 2 A // - вызываем наше слово A для двух ЦЕЛОЧИСЛЕННЫХ значений - 1 и 2
. // - печатаем значение со стека, фактически то, что нам вернула функция A
В данном случае скриптовая машина будет контроллировать как число входящих значений, так и исходящих.

Ну и пример использования слова PROCEDURE:

#!delphi
PROCEDURE // - используем PROCEDURE вместо :
 A
  INTEGER IN aParam1 // - определяем первый параметр
  INTEGER IN aParam2 // - определяем второй параметр
 aParam1 // - получаем значение параметра aParam1
 aParam2 // - получаем значение параметра aParam2
 + // - получаем сумму двух значений
 . // - печатаем полученное значение
; // A

1 2 A // - вызываем наше слово A для двух ЦЕЛОЧИСЛЕННЫХ значений - 1 и 2
Слово PROCEDURE - ГАРАНТИРУЕТ, что наше слово не вернёт НИ ОДНОГО ЗНАЧЕНИЯ.

Подведём итоги.

В данной статье мы рассмотрели ключевые слова :, ;, IN, FUNCTION, PROCEDURE.

А также передачу параметров словам и возврат значений из них.

Также мы слегка коснулись типизации значений.

Кроме типа INTEGER бывают ещё - STRING, OBJECT, CLASS, INTERFACE, CHAR, ARRAY, FILE, BOOLEAN.

А также есть ещё ANY - которое означает "значение любого типа". И PRINTABLE - которое означает "любое значение пригодное для вывода на печать". А также - VOID, который означеает "гарантированное отсутствие значения".

Также есть ещё типы ITERATABLE, ORDINAL и ATOMIC. О них мы также поговорим позже.

Пока лишь пример из аксиоматики:

#!delphi
INTEGER BOOLEAN TYPE ORDINAL
STRING ORDINAL TYPE ATOMIC
ATOMIC TYPE COMPARABLE

FILE ARRAY TYPE ITERATABLE
ITERATABLE ATOMIC CASTABLE INTERFACE TYPE ANY
ANY TYPE VARIANT

ARRAY TYPE ITERATOR

FUNCTOR TYPE VAR_REF
В следующей статье мы рассмотрим "параметры справа". Зачем они нужны и как ими пользоваться.

Надеюсь, что данная статья была вам полезна.

Updated