Паскаль играет в Go. Реализация методов и интерфейсов в любительском компиляторе
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’а при запуске некоторых откомпилированных примеров.