Что такое фантомный тип и как это связано с traits?

Что такое фантомный тип и как это связано с traits?

Много раз натыкался на термин «фантомный тип», особенно в контексте обсуждения traits в языке Scala.

Что это такое? При чём здесь traits?

Заранее прошу извинить за длину ответа, просто иначе понять, что такое фантомные типы и как они используются, будет трудно, а тема и правда интересная.

Предположим, "Роскосмос" попросил написать нас систему управления запуском ракет. "Ракета должна быть заправлена топливом и кислородом, и только после этого можно запускать", - лаконично констатирует техническое задание. Не долго думая, мы пишем следующий код на языке Scala (в функциональном стиле, не в объектно-ориентированном, почему - будет видно впоследствии):

Модуль UnsafeRocketModule определяет тип Rocket, функцию-конструктор ракет (т.к. конструктор типа Rocket не экспортируется модулем в целях поддержания инкапсуляции), а также функции заправки ракеты топливом, кислородом, и функцию запуска ракеты. Мы также отслеживаем состояние ракеты (заправленность топливом и кислородом) и генерируем исключение при попытке запустить неподготовленную ракету. Загрузим этот модуль в Scala REPL, а затем попробуем ввести определение функции, которая подготавливает ракету (заправляет топливом и кислородом), и запускает ее:

Все скомпилировалось без ошибок, можно работать:

Вот незадача: в тексте функции prepareAndLaunchRocket мы заправили ракету топливом, но забыли заправить кислородом! Эта типичная для программ ошибка: попытка выполнить операцию над объектом, который находится в неподходящем для этого состоянии. Этот класс ошибок выявляется во время выполнения программы (run time), и очень часто, уже в процессе эксплуатации. Но мы ведь не хотим, чтобы из-за нашей программы взрывались ракеты, только потому, что в коде мы забыли подготовить ее должным образом, а из-за нехватки времени и внимания написали тесты, не покрывающие этот случай, что не позволило выявить проблему до сдачи в эксплуатацию. Поэтому, исправив функцию prepareAndLaunchRocket, мы понимаем, что пришло время что-то кардинально менять. А именно: мы должны придумать способ гарантировать, что перед запуском (то есть, вызовом функции launch()) ракета будет подготовлена должным образом, то есть будут вызваны обе функции addFuel И addO2. Такая гарантия означает, что попытка запуска неподготовленной ракеты должна теперь выявляться во время компиляции (compile time) программы! Иными словами, компилятор не должен нам позволить скомпилировать ошибочное определение prepareAndLaunchRocket, приведенное выше. А это означает, что это определение не должно пройти проверку типов. То есть, мы должны расширить систему типов, принятую в нашем языке программирования. С этого момента начинается магия.

Первое, что мы делаем, это переписываем определение типа Rocket следующим образом:

Что мы поменяли? Тип Rocket приобрел два параметра типа (Fuel и O2), а атрибуты hasFuel и hasO2 были изъяты. Мы перенесли отслеживание состояния заправленности ракеты топливом и кислородом из системы времени выполнения в систему типов, то есть в систему времени компиляции. То, что отслеживалось атрибутами класса отныне отслеживается параметрами этого типа. Теперь добавим следующие вспомогательные типы:

Эти типы будут использоваться нами как значения времени компиляции вместо значений времени выполнения (true и false для атрибутов hasFuel и hasO2) для индикации состояния заправленности ракеты. Это типы-маркеры, существующие только для подстройки системы типов, они не имеют атрибутов, а их значения (new NoFuel < . >) нами никогда не будут использованы. Такие типы называются фантомными. А traits являются удобным механизмом их определения. С их помощью мы можем переписать оставшиеся функции модуля:

Обратим внимание: потребности в проверке заправленности ракеты функцией launch() больше нет. Система типов гарантирует, что launch() будет вызвана только для корректно подготовленной ракеты. Кроме того, становится понятно, почему нам следует использовать функциональный стиль, а не объектно-ориентированный. В последнем случае объект Rocket был бы создан единственный раз и функции addFuel, addO2 и launch вызывались бы впоследствии как его методы. Нам же необходимо менять тип ракеты при вызове соответствующих операций, и создавать значения этих типов с нуля, что и предполагает функциональный стиль. Загрузим модуль SafeRocketModule в Scala REPL, а затем снова попробуем ввести ошибочное определение функции prepareAndLaunchRocket:

Компилятор не дал ошибочному определению попасть в код программы из-за ошибки во время проверки типов. Исправим определение функции:

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

Рассмотренный способ применения фантомных типов часто используется при проектировании публичных интерфейсов (API) в таких языках, как Haskell, особенно для сложных интерфейсов. В других языках, с менее развитой системой типов, эти приемы не используются вовсе. К примеру, в Java, насколько мне известно, все это не работает "из коробки", и надо делать уродливую "мумбу-юмбу", чтобы получить похожий результат. Также следует заметить, что это не единственная область применения фантомных типов. Более подробно о последних и о возможных областях применения можно почитать здесь (все источники англоязычные):

📎📎📎📎📎📎📎📎📎📎