Dagaz: Факториал — это просто!

в 17:16, , рубрики: game development, Программирование

imageСкриптинг — пожалуй наиболее важная (хотя и не самая сложная) часть задуманного мной проекта. Для того, чтобы всё заработало, мне потребуется язык общего назначения, с переменными, условным выполнением, циклами и исключениями. Мне не требуется что-то сложное, вроде анонимных функций или замыканий. Скорее всего, мне не пригодится даже рекурсия, во всяком случае, пока, для неё не нашлось применений, ни в одном из моих case-ов. В этом языке совсем не будет синтаксического сахара, поскольку все задачи метапрограмирования возьмёт на себя XSLT. В общем, этот язык будет прост настолько, насколько это возможно, но… не проще. 

Напомню, что скрипт, в моём понимании, содержит не только определение ряда функций, задающих «поведение» участвующих в игре объектов, но, помимо этого, должен определять структуру довольно таки сложных объектов, таких как «доска», «фигуры» и т.п. Вот пример подобного описания:

Определение ''доски''

   (board
      (grid (dimensions "A_J" "1")
            (direction (name forward)   1 0)
            (direction (name backward) -1 0)
      )
      (grid (dimensions "A_J" "2")
            (direction (name forward)  -1 0)
            (direction (name backward)  1 0)
      )
      (grid (dimensions "A_K" "3")
            (direction (name forward)   1 0)
            (direction (name backward) -1 0)
      )
      (grid (dimensions "a_d"))
      (link (name forward)  (J1 J2) (A2 A3) (K3 K3))
      (link (name backward) (J2 J1) (A3 A2) (K3 K3))
      (zone (name rebirth)  (positions F2))
      (zone (name protect)  (positions F3 G3 H3 I3 J3))
      (zone (name beauty)   (positions F3))
      (zone (name water)    (positions G3))
      (zone (name truth)    (positions H3))
      (zone (name ra)       (positions I3))
      (zone (name stop)     (positions F3 K3))
      (zone (name end)      (positions K3))
      (zone (name dices)    (positions a b c d))
   )

Это определение — своего рода «карта» настольной игры и оно может быть очень сложным. Именно для разбора таких иерархических структур мне понадобился XPath, что, в свою очередь, привело к необходимости использования XML, для внутреннего представления скрипта в памяти. Разбор таких структур многословен, но довольно прямолинеен. Код — другое дело.

(define (factorial n a)
  (if (<= n 1)
     a
   else
     (factorial (- n 1) (* a n))
  )
)

(define (factorial n)
  (factorial n 1)
)

(define main
  (set! out (factorial 5))
)

Надеюсь, вы его узнали. Да, здесь вычисляется значение факториала, с использованием «хвостовой рекурсии», и нет, я не собираюсь заниматься оптимизацией «хвостовой рекурсии». Как я уже говорил, мне и рекурсия то вряд ли понадобится. Тем, кто вдруг не знает, что такое «хвостовая рекурсия» и зачем её может понадобиться оптимизировать, я рекомендую почитать эту замечательную книгу.

Этот код — просто компактный тест, максимально покрывающий весь интересующий меня, в данный момент, функционал. В нём используется условный оператор, вызовы и определения функций, перегрузка и работа с параметрами. Поскольку операторы ввода/вывода в языке не предусмотрены (они и не потребуются), в тесте, вывод будет эмулироваться присвоением итогового значения «переменной» out.

Кстати говоря

Сложность выбранного теста вполне себя оправдала. Вычисляя факториал трёх я обратил внимание на то, что полученное значение «2» несколько отличается от ожидаемого. Расследование этой ситуации привело к следующему фиксу. Я вполне мог не заметить эту ошибку, используя более простой тест.

Можно заметить, что все управляющие конструкции языка являются функциями, в том смысле, что они всегда возвращают некое значение. Например, результатом выполнения последовательности выражений является результат последнего выполненного выражения. Отдельно стоит упомянуть об исключениях:

(define dice-drop
   (check (in-zone? dices))
   (check is-empty?)
   drop-pieces
   add-move
)

В любой момент, может быть выполнена проверка истинности некоего булевского выражения. В случае, если полученное значение окажется ложным, выполнение должно быть немедленно прервано. Мне неизвестен инструмент, более подходящий для этой задачи, чем исключения. CheckExpression вычисляет значение своего аргумента в булевском контексте и «бросает» CheckException, при нарушении условия (очевидно, что эта функция будет возвращать истинное значение всегда, если только до возврата значения вообще дойдёт дело).

Значения аргументов функций будут вычисляться строго, до передачи управления функциям. Тотальной "ленивости" вычислений не будет, но такие функции как if, and или or будут вычислять значения своих аргументов лишь при необходимости. Для вызова произвольных функций (определённых в теле скрипта), предназначена конструкция ApplyExpression. Полученные значения аргументов представляют собой ни что иное, как локальные переменные, область видимости которых ограничена вызываемой функцией. Доступ ко всем переменным осуществляется через интерфейс IEnvironment, передаваемый каждому вычисляемому выражению:

public interface IEnvironment {
	...
	void   letValue(String name, IValue value) throws EvaluationException;
	void   setValue(String name, IValue value) throws EvaluationException;
	IValue getValue(String name, boolean isQuoted) throws ValueNotFoundException;
	...
}

Используя этот интерфейс, можно создавать локальные переменные (функцией let), изменять их значение (функцией set!), а также получать значение по имени. Для последней их этих операций, специальной функции не предусмотрено. Любой атом, не являющийся строковым или числовым литералом, будет рассматриваться как обращение к переменной (или параметру функции), при условии того, что в скрипте не была определена функция с тем же именем и арностью.

Подробности для любознательных

Коллизий между вызовами функций и обращениями к переменным можно было бы избежать, всегда обрамляя вызовы функций скобками, следующим образом:

(define dice-drop
   ...
   (drop-pieces)
   (add-move)
)

Я посчитал подобную запись излишне громоздкой. Кроме того, возникла проблема с внутренним представлением подобных структур в XML. Другой не реализованной возможностью является вычисление имени вызываемой функции:

((if (< a b) "+" else "-") a b)

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

С целью максимального упрощения языка, я не стал реализовывать оператор QUOTE явно (поэтому написать Quine на этом языке вряд ли удастся). Цитирование в языке есть, но оно неявное. Например, такие выражения как let и set! «знают», что первый аргумент вычислять не надо (за это отвечает переопределённый метод IExpression.isQuoted). Таким образом, первый аргумент, в этих выражениях, воспринимается как имя переменной, а не попытка получения её значения.

Ещё больше подробностей
В прикладном коде могут встречаться и более сложные ситуации, связанные с неявным цитированием:

(define (piece-move direction)
   (check up)
   (check direction)
   ...
)
...
(moves 
   (piece-move north)
   (piece-move south)
   (piece-move east)
   (piece-move west)
   ...
)

Подобный код будет встречаться очень часто. Здесь, атомы north, south, east и west должны интерпретироваться как имена направлений, но лишь при условии, что такие направления на доске определены (в противном случае, это обращение к переменным). Таким образом, параметр direction будет содержать строку с именем соответствующего направления.

Позиционирование и навигация осуществляются путем обращения к псевдопеременным с соответствующими именами. Обращение к «переменной» up (при условии определения такого направления на доске) переместит маркер текущей позиции (в качестве побочного эффекта), а также вернёт значение true, если это перемещение было выполнено успешно.

Но что произойдёт при обращении к параметру direction? Если не предпринимать дополнительных действий, будет получено не пустое строковое значение, которое, в булевском контексте, будет интерпретировано как true. Это явно не то, чего бы нам хотелось. Чтобы исправить ситуацию, модуль, предоставляющий доступ к состоянию расчёта хода, должен проверить не является ли полученная строка именем позиции или направления. Если это так, операция get должна быть выполнена повторно и уже её результат должен быть возвращён в точку вызова. Это является хорошей иллюстрацией тезиса о том, что упрощение языка может вести к усложнению его реализации.

С учётом всего сказанного выше, кодогенерация становится довольно тривиальной. Требуется лишь найти определения функций в XML и рекурсивно построить иерархии соответствующих им выражений. Сами выражения создаются ExpressionFactory, что позволяет легко расширять список поддерживаемых системных функций (вплоть до реализации новых конструкций языка).

Разумеется, это не конец пути. Чтобы от такого скриптинга была польза, необходимо связать его с навигацией в игровом пространстве и изменением состояния расчёта хода. Потребуется определить массу системных функций, работающих в терминах предметной области, таких как is-friend? или is-empty?. Наконец, для реализации генератора ходов, потребуется реализовать возможность недетерминированных вычислений. По сравнению со всеми этими вещами, факториал — это действительно просто.

Автор: GlukKazan

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js