If I could export one feature of Go into other languages, it would be interfaces. — Russ Cox



Мой предельно простой компилятор Паскаля уже становился предметом двух публикаций на Хабре. Со времени их написания язык обзавёлся всеми недостающими средствами, положенными стандартному Паскалю, и многими плюшками, добавленными в Паскаль компанией Borland в её золотую пору. Компилятор также научился ряду простейших локальных оптимизаций, достаточных хотя бы для того, чтобы глаза не кровоточили при взгляде на листинг дизассемблера.

Тем не менее дебри объектно-ориентированного программирования остались совершенно нетронутыми. Так почему бы компилятору не послужить теперь полигоном для экспериментов в этой области? И почему бы нам не почерпнуть вдохновение из слов Расса Кокса, вынесенных в эпиграф? Попробуем реализовать в Паскале методы и интерфейсы в стиле Go. Затея интересна хотя бы тем, что все популярные в прошлом компиляторы Паскаля (Delphi, Free Pascal) по сути заимствовали объектную модель из C++. Любопытно посмотреть, как на той же почве приживётся совсем иной подход, позаимствованный из Go. Если вы вслед за мной готовы запастись изрядной долей иронии, отбросить вопрос «Зачем?» и воспринять происходящее как игру, добро пожаловать под кат.

Принципы


Под «стилем Go» будем понимать несколько принципов, на основе которых внедрим методы и интерфейсы в Паскаль:

  • Не существует самостоятельных понятий класса, объекта, наследования.
  • Метод можно реализовать для любого конкретного типа данных. Для этого не требуется изменять объявление самого типа.
  • С интерфейсом совместим любой конкретный тип данных, для которого реализованы все методы, перечисленные в объявлении интерфейса. В объявлении конкретного типа данных не требуется указывать, что он реализует интерфейс.

Реализация


Для объявления методов и интерфейсов используются в новой роли стандартные ключевые слова Паскаля for и interface. Никаких новых ключевых слов не вводится. Слово for служит для указания имени и типа получателя метода (в терминологии Go). Вот пример описания метода для предварительно объявленного типа TCat с полем Name:

procedure Greet for c: TCat (const HumanName: string);
  begin
  WriteLn('Meow, ' + HumanName + '! I am ' + c.Name);
  end;

Получатель фактически является первым аргументом метода.

Интерфейс представляет собой обычную запись Паскаля, в объявлении которой слово record заменяется словом interface. В этой записи не допускается объявлять никакие поля, кроме полей процедурного типа. Помимо этого, в начало записи добавляется скрытое поле Self. В нём хранится указатель на данные того конкретного типа, который приводится к интерфейсному типу. Вот пример объявления интерфейса:

type
  IPet = interface
    Greet: procedure (const HumanName: string);
  end;

Приведение к интерфейсному типу всегда делается явно:

Pet := IPet(Cat);

При этом компилятор проверяет наличие всех методов, требуемых интерфейсом, и совпадение их сигнатур. Затем он устанавливает указатель Self, заполняет все процедурные поля интерфейса указателями на методы конкретного типа.

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

Пример


Неплохим примером использования интерфейсов может послужить программа рендеринга трёхмерных сцен методом обратной трассировки лучей. Сцена состоит из простых геометрических тел: параллелепипедов, сфер и т. п. Каждый луч, испущенный из глаза наблюдателя, требуется отследить (через все его отражения) до попадания в источник света или ухода в бесконечность. Для этого каждому виду тел приписывается метод Intersect, вычисляющий координаты точки попадания луча на поверхность тела и компоненты нормали в этой точке. Реализация этого метода для разных видов тел различна. Соответственно, информацию о телах удобно хранить в массиве интерфейсных записей Body, причём для всех элементов массива поочерёдно вызывается метод Intersect. Интерфейс перенаправляет этот вызов на конкретный метод в зависимости от вида тела.

Вот так может выглядеть сцена, построенная описанным способом:



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

Итоги


Простейшая реализация полиморфизма в собственном компиляторе Паскаля оказалась нетрудным делом, быстро принесшим первые плоды. Некоторых осложнений можно ожидать в задаче динамического определения конкретного типа данных, который был приведён к интерфейсному типу. Усилий потребует также устранение неочевидных конфликтов с механизмами проверки типов стандартного Паскаля. Наконец, помимо всех забот об интерфейсах продолжается неравная борьба с Microsoft вокруг ложных тревог их Windows Defender'а при запуске некоторых откомпилированных примеров.

Комментарии (2)


  1. KvanTTT
    05.12.2019 12:54

    Заголовок и картинка не соответствуют содержимому статьи, так как об игре речи не идет.


    1. Tereshkov Автор
      05.12.2019 13:12
      -1

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