Много раз натыкался на термин «фантомный тип», особенно в контексте обсуждения 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(
// ...
}
Что мы поменяли? Тип 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()))
| }
scala> prepareAndLaunchRocket()
3-2-1... Пуск!
Компиляция прошла успешно, пробный запуск осуществлен без ошибок. Теперь в эксплуатацию попадет гарантированно корректная версия этой функции, даже при отсутствии тестов - верификацию выполнила за нас система типов. Наша новая реализация имеет также и другие преимущества, по сравнению со старой: расширенная нами система типов гарантирует, что ракета будет заправлена топливом и кислородом только один раз, не допуская повторных вызовов функций заправки для ракеты, которая уже была заправлена:
scala> addFuel(addFuel(createRocket))
scala> addO2(addO2(createRocket))
Комментариев нет:
Отправить комментарий