Страницы

Поиск по вопросам

суббота, 13 октября 2018 г.

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

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


Ответ

Заранее прошу извинить за длину ответа, просто иначе понять, что такое фантомные типы и как они используются, будет трудно, а тема и правда интересная. Предположим, "Роскосмос" попросил написать нас систему управления запуском ракет. "Ракета должна быть заправлена топливом и кислородом, и только после этого можно запускать", - лаконично констатирует техническое задание. Не долго думая, мы пишем следующий код на языке Scala (в функциональном стиле, не в объектно-ориентированном, почему - будет видно впоследствии): object UnsafeRocketModule { case class Rocket private[UnsafeRocketModule] (hasFuel: Boolean, hasO2: Boolean)
def createRocket() = Rocket(false, false) def addFuel(r: Rocket) = Rocket(true, r.hasO2) def addO2(r: Rocket) = Rocket(r.hasFuel, true) def launch(r: Rocket) = if (!r.hasFuel || !r.hasO2) throw new Exception("Попытка запустить неподготовленную ракету!") else println("3-2-1... Пуск!") } Модуль UnsafeRocketModule определяет тип Rocket, функцию-конструктор ракет (т.к. конструктор типа Rocket не экспортируется модулем в целях поддержания инкапсуляции), а также функции заправки ракеты топливом, кислородом, и функцию запуска ракеты. Мы также отслеживаем состояние ракеты (заправленность топливом и кислородом) и генерируем исключение при попытке запустить неподготовленную ракету. Загрузим этот модуль в Scala REPL, а затем попробуем ввести определение функции, которая подготавливает ракету (заправляет топливом и кислородом), и запускает ее: scala> def prepareAndLaunchRocket() { | import UnsafeRocketModule._ | launch(addFuel(createRocket())) | } prepareAndLaunchRocket: ()Unit Все скомпилировалось без ошибок, можно работать: scala> prepareAndLaunchRocket() java.lang.Exception: Попытка запустить неподготовленную ракету! at UnsafeRocketModule$.launch(:13) at $anonfun$prepareAndLaunchRocket$2.apply(:8) at $anonfun$prepareAndLaunchRocket$2.apply(:8) ... пропущено ... Вот незадача: в тексте функции prepareAndLaunchRocket мы заправили ракету топливом, но забыли заправить кислородом! Эта типичная для программ ошибка: попытка выполнить операцию над объектом, который находится в неподходящем для этого состоянии. Этот класс ошибок выявляется во время выполнения программы (run time), и очень часто, уже в процессе эксплуатации. Но мы ведь не хотим, чтобы из-за нашей программы взрывались ракеты, только потому, что в коде мы забыли подготовить ее должным образом, а из-за нехватки времени и внимания написали тесты, не покрывающие этот случай, что не позволило выявить проблему до сдачи в эксплуатацию. Поэтому, исправив функцию prepareAndLaunchRocket, мы понимаем, что пришло время что-то кардинально менять. А именно: мы должны придумать способ гарантировать, что перед запуском (то есть, вызовом функции launch()) ракета будет подготовлена должным образом, то есть будут вызваны обе функции addFuel И addO2. Такая гарантия означает, что попытка запуска неподготовленной ракеты должна теперь выявляться во время компиляции (compile time) программы! Иными словами, компилятор не должен нам позволить скомпилировать ошибочное определение prepareAndLaunchRocket, приведенное выше. А это означает, что это определение не должно пройти проверку типов. То есть, мы должны расширить систему типов, принятую в нашем языке программирования. С этого момента начинается магия. Первое, что мы делаем, это переписываем определение типа Rocket следующим образом: object SafeRocketModule { case class Rocket[Fuel, O2] private[SafeRocketModule] ()
// ... } Что мы поменяли? Тип Rocket приобрел два параметра типа (Fuel и O2), а атрибуты hasFuel и hasO2 были изъяты. Мы перенесли отслеживание состояния заправленности ракеты топливом и кислородом из системы времени выполнения в систему типов, то есть в систему времени компиляции. То, что отслеживалось атрибутами класса отныне отслеживается параметрами этого типа. Теперь добавим следующие вспомогательные типы: sealed trait NoFuel sealed trait HasFuel sealed trait NoO2 sealed trait HasO2 Эти типы будут использоваться нами как значения времени компиляции вместо значений времени выполнения (true и false для атрибутов hasFuel и hasO2) для индикации состояния заправленности ракеты. Это типы-маркеры, существующие только для подстройки системы типов, они не имеют атрибутов, а их значения (new NoFuel { ... }) нами никогда не будут использованы. Такие типы называются фантомными. А traits являются удобным механизмом их определения. С их помощью мы можем переписать оставшиеся функции модуля: def createRocket() = Rocket[NoFuel, NoO2]() def addFuel[O2](r: Rocket[NoFuel,O2]) = Rocket[HasFuel,O2]() def addO2[Fuel](r: Rocket[Fuel,NoO2]) = Rocket[Fuel,HasO2]() def launch(r: Rocket[HasFuel,HasO2]) = println("3-2-1... Пуск!") Обратим внимание: потребности в проверке заправленности ракеты функцией launch() больше нет. Система типов гарантирует, что launch() будет вызвана только для корректно подготовленной ракеты. Кроме того, становится понятно, почему нам следует использовать функциональный стиль, а не объектно-ориентированный. В последнем случае объект Rocket был бы создан единственный раз и функции addFuel, addO2 и launch вызывались бы впоследствии как его методы. Нам же необходимо менять тип ракеты при вызове соответствующих операций, и создавать значения этих типов с нуля, что и предполагает функциональный стиль. Загрузим модуль SafeRocketModule в Scala REPL, а затем снова попробуем ввести ошибочное определение функции prepareAndLaunchRocket: scala> def prepareAndLaunchRocket() { | import SafeRocketModule._ | launch(addFuel(createRocket())) | } :8: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.NoO2] required: SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.HasO2] launch(addFuel(createRocket())) ^ Компилятор не дал ошибочному определению попасть в код программы из-за ошибки во время проверки типов. Исправим определение функции: scala> def prepareAndLaunchRocket() { | import SafeRocketModule._ | launch(addFuel(addO2(createRocket()))) | } prepareAndLaunchRocket: ()Unit
scala> prepareAndLaunchRocket() 3-2-1... Пуск! Компиляция прошла успешно, пробный запуск осуществлен без ошибок. Теперь в эксплуатацию попадет гарантированно корректная версия этой функции, даже при отсутствии тестов - верификацию выполнила за нас система типов. Наша новая реализация имеет также и другие преимущества, по сравнению со старой: расширенная нами система типов гарантирует, что ракета будет заправлена топливом и кислородом только один раз, не допуская повторных вызовов функций заправки для ракеты, которая уже была заправлена: scala> addFuel(addFuel(createRocket)) :10: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.HasFuel,SafeRocketModule.NoO2] required: SafeRocketModule.Rocket[SafeRocketModule.NoFuel,?] addFuel(addFuel(createRocket)) ^
scala> addO2(addO2(createRocket)) :10: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.HasO2] required: SafeRocketModule.Rocket[?,SafeRocketModule.NoO2] addO2(addO2(createRocket)) ^ Рассмотренный способ применения фантомных типов часто используется при проектировании публичных интерфейсов (API) в таких языках, как Haskell, особенно для сложных интерфейсов. В других языках, с менее развитой системой типов, эти приемы не используются вовсе. К примеру, в Java, насколько мне известно, все это не работает "из коробки", и надо делать уродливую "мумбу-юмбу", чтобы получить похожий результат. Также следует заметить, что это не единственная область применения фантомных типов. Более подробно о последних и о возможных областях применения можно почитать здесь (все источники англоязычные): Phantom Types In Haskell and Scala. Пример с ракетами был адаптирован из этой статьи, где параллельно приводится версия на Haskell. Phantom type (HaskellWiki) A Foundation for Embedded Languages by Morten Rhiger - основательная академическая публикация о фантомных типах и их применениях в Haskell

Комментариев нет:

Отправить комментарий