Many times I came across the term "phantom type", especially in the context of the discussion of traits in the Scala language.
What it is? What does the traits ?
Many times I came across the term "phantom type", especially in the context of the discussion of traits in the Scala language.
What it is? What does the traits ?
I apologize in advance for the length of the answer, just otherwise it will be difficult to understand what phantom types are and how they are used, and the topic is really interesting.
Suppose Roskosmos asked us to write a missile launch control system. "The rocket must be filled with fuel and oxygen, and only then can it be launched," concisely states the technical task. Without hesitation, we write the following code in the Scala language (in a functional style, not in object-oriented, why - it will be seen later):
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... Пуск!") }
The UnsafeRocketModule module defines the type of Rocket, the rocket designer function (since the Rocket type constructor is not exported by the module in order to maintain encapsulation), as well as the function of refueling the rocket with fuel, oxygen, and the function of launching the rocket. We also monitor the state of the rocket (fueling and oxygen) and generate an exception when trying to launch an unprepared rocket. Let's load this module into the Scala REPL, and then we will try to enter the definition of the function that prepares the rocket (fills it with fuel and oxygen) and starts it:
scala> def prepareAndLaunchRocket() { | import UnsafeRocketModule._ | launch(addFuel(createRocket())) | } prepareAndLaunchRocket: ()Unit
Everything compiled without errors, you can work:
scala> prepareAndLaunchRocket() java.lang.Exception: Попытка запустить неподготовленную ракету! at UnsafeRocketModule$.launch(<console>:13) at $anonfun$prepareAndLaunchRocket$2.apply(<console>:8) at $anonfun$prepareAndLaunchRocket$2.apply(<console>:8) ... пропущено ...
Here is a bad luck: in the text of the prepareAndLaunchRocket function, we fueled the rocket with fuel, but forgot to fill it with oxygen! This is a typical program error: an attempt to perform an operation on an object that is in the wrong state. This class of errors is detected during the execution of the program (run time) , and very often, already in operation. But we don’t want the rockets to explode because of our program, just because we forgot to prepare it properly in the code, and because of the lack of time and attention, we wrote tests that didn’t cover this case, which didn’t allow us to identify the problem before commissioning. Therefore, having corrected the prepareAndLaunchRocket function, we understand that the time has come to change something drastically. Namely: we need to think of a way to ensure that before launch (that is, by calling the launch () function) the rocket will be properly prepared, that is, both addFuel and addO2 will be called. Such a guarantee means that an attempt to launch an unprepared rocket should now be detected during the compile time of the program ! In other words, the compiler should not allow us to compile the erroneous definition of prepareAndLaunchRocket given above. This means that this definition should not pass type checking. That is, we must extend the type system adopted in our programming language. From this moment begins the magic.
The first thing we do is rewrite the definition of the Rocket type as follows:
object SafeRocketModule { case class Rocket[Fuel, O2] private[SafeRocketModule] () // ... }
What have we changed? The Rocket type acquired two type parameters (Fuel and O2), and the hasFuel and hasO2 attributes were removed. We transferred the tracking of the state of fuel filling of the rocket with fuel and oxygen from the runtime system to the type system, that is, to the compile time system. What was tracked by class attributes is now tracked by parameters of this type. Now add the following auxiliary types:
sealed trait NoFuel sealed trait HasFuel sealed trait NoO2 sealed trait HasO2
These types will be used by us as compile-time values instead of run-time values (true and false for hasFuel and hasO2 attributes) to indicate the status of a rocket charge. These are marker types that exist only for adjusting the type system, they have no attributes, and we will never use their values (new NoFuel {...}). Such types are called phantom . And traits are a convenient mechanism for their determination. With their help, we can rewrite the remaining functions of the module:
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... Пуск!")
Note: there is no longer any need to test launch of the rocket with the launch () function. The type system ensures that launch () is called only for a properly prepared rocket. In addition, it becomes clear why we should use a functional style, rather than an object-oriented one. In the latter case, the Rocket object would be created only once, and the addFuel, addO2, and launch functions would later be called as its methods. We need to change the type of the rocket when invoking the corresponding operations, and create values of these types from scratch, which implies a functional style. Load the SafeRocketModule module into the Scala REPL, and then try again to enter the erroneous definition of the prepareAndLaunchRocket function:
scala> def prepareAndLaunchRocket() { | import SafeRocketModule._ | launch(addFuel(createRocket())) | } <console>:8: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.NoO2] required: SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.HasO2] launch(addFuel(createRocket())) ^
The compiler did not give an erroneous definition to get into the program code due to an error during type checking. Fix the function definition:
scala> def prepareAndLaunchRocket() { | import SafeRocketModule._ | launch(addFuel(addO2(createRocket()))) | } prepareAndLaunchRocket: ()Unit scala> prepareAndLaunchRocket() 3-2-1... Пуск!
The compilation was successful, the test run was carried out without errors. Now the guaranteed version of this function will be put into operation, even in the absence of tests - verification was carried out for us by the type system. Our new implementation also has other advantages over the old one: the type system expanded by us ensures that the rocket will be filled with fuel and oxygen only once, avoiding repeated calls to the refueling functions for a rocket that has already been filled:
scala> addFuel(addFuel(createRocket)) <console>:10: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.HasFuel,SafeRocketModule.NoO2] required: SafeRocketModule.Rocket[SafeRocketModule.NoFuel,?] addFuel(addFuel(createRocket)) ^ scala> addO2(addO2(createRocket)) <console>:10: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.HasO2] required: SafeRocketModule.Rocket[?,SafeRocketModule.NoO2] addO2(addO2(createRocket)) ^
The considered way of using phantom types is often used when designing public interfaces (APIs) in languages such as Haskell, especially for complex interfaces. In other languages with a less developed type system, these techniques are not used at all. For example, in Java, as far as I know, all this does not work out of the box, and it is necessary to do an ugly mumba jumba to get a similar result. It should also be noted that this is not the only application of phantom types. In more detail about the last and about possible scopes it is possible to esteem here (all sources English-speaking):
Source: https://ru.stackoverflow.com/questions/10254/
All Articles