Skip to content

Commit 6cd8a21

Browse files
committed
Add Phantom type docs
1 parent 49baa86 commit 6cd8a21

File tree

5 files changed

+259
-0
lines changed

5 files changed

+259
-0
lines changed

docs/docs/reference/phantom-types.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
---
2+
layout: doc-page
3+
title: "Phantom Types"
4+
---
5+
6+
Phantom Types :ghost:
7+
======================
8+
9+
10+
What is a phantom type?
11+
-----------------------
12+
13+
A phantom type is a manifestation of abstract type that has no effect on the runtime.
14+
These are useful to prove static properties of the code using type evidences.
15+
As they have no effect on the runtime they can be erased from the resulting code by
16+
the compiler once it has shown the constraints hold.
17+
18+
When saying that a they have no effect on the runtime we do not only mean side effects
19+
like IO, field mutation, exceptions and so on. We also imply that if a function receives
20+
a phantom its result will not be affected by this argument.
21+
22+
As phantom do not live at runtime they cannot be subtypes of `scala.Any`, which deffines
23+
methods such as `hashCode`, `equals`, `getClass`, `asInstanceOf` and `isInstanceOf`.
24+
All these operations cannot exist on phantoms as there will not be an underlying object
25+
instance at runtime. At first glace this could look like a limitation, but in fact not
26+
having `asInstanceOf` will make constraints more reliable as it will not be possible to
27+
downcast a phantom value to fake an evidence.
28+
29+
If they don't live in the universe bounded by `scala.Any` and `scala.Nothing` where do
30+
they live? The answer is in their own type universes bounded by their phantom `Any` and `Nothing`.
31+
In fact we allow multiple phantom universes to exist.
32+
33+
```
34+
+-----+ +---------------+ +--------------------+
35+
| Any | | MyPhantom.Any | | MyOtherPhantom.Any |
36+
+-----+ +---------------+ +--------------------+
37+
| | |
38+
+----+------+ +-----+------+ ...
39+
| | | |
40+
+--------+ +--------+ +------+ +--------+
41+
| AnyRef | | AnyVal | | Inky | | Blinky |
42+
+--------+ +--------+ +------+ +--------+
43+
... ... | |
44+
+------+ | +-------+ |
45+
| Null | | | Pinky | |
46+
+------+ | +-------+ |
47+
| | | |
48+
+------+-----+ +----+------+ ...
49+
| | |
50+
+---------+ +-------------------+ +------------------------+
51+
| Nothing | | MyPhantom.Nothing | | MyOtherPhantom.Nothing |
52+
+---------+ +-------------------+ +------------------------+
53+
```
54+
55+
Inside a universe it types support the full Dotty type system. But we cannot mix types from
56+
different universes with `&`, `|` or in type bounds. Each type must be fully defined one universe.
57+
58+
59+
Implement your own phantom type
60+
-------------------------------
61+
Phantom types are definded by an `object` extending `scala.Phantom`. This object will represent
62+
a universe of phantom types that is completely separated from types in `scala.Any` or other
63+
phantom universes. We can define our phantom universe `MyPhantoms`.
64+
65+
```scala
66+
object MyPhantoms extends Phantom
67+
```
68+
69+
```scala
70+
package scala
71+
trait Phantom { // only an `object` can extend this trait
72+
protected final type Any // not a subtype of scala.Any
73+
protected final type Nothing // subtype of every subtype of this.Any
74+
protected final def assume: this.Nothig
75+
}
76+
```
77+
78+
The types in the phantom universe are defined by the top type `Phantom.Any` and bottom type
79+
`Phantom.Nothing`. This means that `MyPhantoms.Any` and `MyPhantoms.Nothing` are the bounds
80+
of the phantom types in `MyPhantoms`, these bounds are `protected` and can not be accessed
81+
from outside `MyPhantoms` unless an alias is defined for them.
82+
83+
New phantom types can be defined using `type XYZ <: OtherPhantom` (where `>: MyPhantom.Nothing`
84+
will be inferred), this would be the equivalent of `class XYZ extends OtherClass` on a types
85+
only (no runtime definitions). Or aliased with `type MyAny = OtherPhantom`. Whitin `MyPhantoms`
86+
it is possible to refer to `MyPhantoms.Any` and `MyPhantoms.Nothing` with `this.Any` and
87+
`this.Nothing` (or just `Any` and `Nothing` but not recommended). Using this we will define
88+
four the four phantoms: `Inky`, `Blinky`, `Pinky` and `Clyde`.
89+
90+
```scala
91+
object MyPhantoms extends Phantom {
92+
type Inky <: this.Any
93+
type Blinky <: this.Any
94+
type Pinky <: Inky
95+
type Clyde <: Pinky
96+
}
97+
```
98+
99+
Values of phantom type can be created using the `protected def assume`. This value can be
100+
used as a value of this phantom type as it's type is `this.Nothig` (or `MyPhantoms.Nothing`).
101+
Usually this value will be used to define a `implicit def` that returns the phantom with a more
102+
precise type. In our example we will only create values of type `Pinky` and `Clyde`
103+
104+
```scala
105+
object MyPhantoms extends Phantom {
106+
... // Type definition
107+
108+
def pinky: Pinky = assume
109+
def clyde: Clyde = assume
110+
}
111+
```
112+
113+
### Using the phantoms
114+
115+
From the user of the phantom type there is almost no difference, except for stronger type guarantees.
116+
We can look at the following simple application:
117+
118+
```scala
119+
import MyPhantoms._
120+
object MyApp {
121+
def run(phantom: Inky) = println("run")
122+
def hide(phantom: Blinky) = println("run")
123+
124+
run(pinky)
125+
run(clyde)
126+
}
127+
```
128+
129+
Note given the way we defined the phantoms it is impossible to call the `hide` as we did not
130+
expose any value of type `Blinky` to the user. We cannot call `hide(MyPhantoms.assume)` as
131+
`assume` is protected, `hide(null.asInstanceOf[Blinky])` does not compile because it is impossible
132+
to cast to a phantom and `hide(throw new Exception)` (or `hide(???)`) does not compile as `throw` of
133+
type `scala.Nothing` is not in the same type universe as `Blinky`. Good, we caught all possible
134+
mistakes before when compiling the code, no surprises at runtime (hopefully not in production).
135+
136+
137+
What happens with Phantoms at runtime?
138+
--------------------------------------
139+
140+
Disclaimer: Most of phantom erasure is implemented, but not all of is has been merged in `dotty/master` yet.
141+
142+
As phantom have no effect on the result of a method invocation we just remove them for the call an definition.
143+
The evaluation of the phantom parameter is still be done unless it can be optimized away.
144+
By removing them we also restrict overloading as `def f()` and `def f(x: MyPhantom)` will
145+
have the same signature in the bytecode, just use different names to avoid this.
146+
147+
At runtime the `scala.Phantom` trait will not exist.
148+
* The object extending `Phantom` will not extend it anymore
149+
* All phantom types will be erased on a single erased type (important in overloading for methods returning a phantom)
150+
* Calls to `Phantom.assume` will become a reference to a singleton of the erased phantom type and will be removed wherever possible
151+
152+
```scala
153+
object MyOtherPhantom extends Phantom {
154+
type MyPhantom <: this.Any
155+
def myPhantom: MyPhantom = assume
156+
157+
def f1(a: Int, b: MyPhantom, c: Int): Int = a + c
158+
159+
def f2 = {
160+
f1(3, myPhantom, 2)
161+
}
162+
}
163+
```
164+
165+
will be compiled to
166+
167+
```scala
168+
object MyOtherPhantom {
169+
def myPhantom(): <ErasedPhantom> = <ErasedPhantom.UNIT>
170+
171+
def f1(a: Int, c: Int): Int = a + c
172+
173+
def f2 = {
174+
val a$ = 3
175+
myPhantom()
176+
val b$ = 3
177+
f1(a$, b$)
178+
}
179+
}
180+
```
181+
182+
Note that `myPhantom` is not removed as it could have some side effect before returning the phantom.
183+
To remove it just use `inline def myPhantom` instead this will remove the call and allow the
184+
`<ErasedPhantom.UNIT>` to be optimized away.

docs/sidebar.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ sidebar:
2525
url: docs/reference/type-lambdas.html
2626
- title: Implicit Function Types
2727
url: docs/reference/implicit-function-types.html
28+
- title: Phantom Types
29+
url: docs/reference/phantom-types.html
2830
- title: Enums
2931
subsection:
3032
- title: Enumerations
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
object MyPhantoms extends Phantom {
2+
type Inky <: this.Any
3+
type Blinky <: this.Any
4+
type Pinky <: Inky
5+
type Clyde <: Pinky
6+
7+
def pinky: Pinky = assume
8+
def clyde: Clyde = assume
9+
}
10+
11+
import MyPhantoms._
12+
object MyApp {
13+
def run(phantom: Inky) = println("run")
14+
def hide(phantom: Blinky) = println("run")
15+
16+
run(pinky)
17+
run(clyde)
18+
19+
hide(pinky) // error
20+
hide(clyde) // error
21+
hide(MyPhantoms.assume) // error
22+
hide(throw new Exception) // error
23+
hide(???) // error
24+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
object MyPhantoms extends Phantom {
2+
type Inky <: this.Any
3+
type Blinky <: this.Any
4+
type Pinky <: Inky
5+
type Clyde <: Pinky
6+
7+
def pinky: Pinky = assume
8+
def clyde: Clyde = assume
9+
}
10+
11+
import MyPhantoms._
12+
object MyApp {
13+
def run(phantom: Inky) = println("run")
14+
def hide(phantom: Blinky) = println("run")
15+
16+
run(pinky)
17+
run(clyde)
18+
19+
hide(null.asInstanceOf[Blinky]) // error
20+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
object MyPhantoms extends Phantom {
2+
type Inky <: this.Any
3+
type Blinky <: this.Any
4+
type Pinky <: Inky
5+
type Clyde <: Pinky
6+
7+
def pinky: Pinky = assume
8+
def clyde: Clyde = assume
9+
}
10+
11+
import MyPhantoms._
12+
object MyApp {
13+
def run(phantom: Inky) = println("run")
14+
def hide(phantom: Blinky) = println("run")
15+
16+
run(pinky)
17+
run(clyde)
18+
}
19+
20+
object MyOtherPhantom extends Phantom {
21+
type MyPhantom <: this.Any
22+
def myPhantom: MyPhantom = assume
23+
24+
def f1(a: Int, b: MyPhantom, c: Int): Int = a + c
25+
26+
def f2 = {
27+
f1(3, myPhantom, 2)
28+
}
29+
}

0 commit comments

Comments
 (0)