Skip to content

Add Phantom type docs #2519

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions docs/_includes/features.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,12 @@ <h1 id="so-features">So, features?</h1>
<td>Implemented</td>
</tr>
<tr>
<td>
<!--<a href="http://dotty.epfl.ch/docs/reference/phantom-types.html">Phantom types</a>-->
Phantom types
</td>
<td><a href="http://dotty.epfl.ch/docs/reference/implicit-function-types.html">Implicit function types</a></td>
<td>Implemented</td>
</tr>
<tr>
<td><a href="http://dotty.epfl.ch/docs/reference/implicit-function-types.html">Implicit function types</a></td>
<td>Implemented</td>
<td><a href="http://dotty.epfl.ch/docs/reference/phantom-types.html">Phantom types</a></td>
<td>In progress</td>
</tr>
<tr>
<td><a href="https://github.com/dotty-linker/dotty">Auto-Specialization</a></td>
Expand Down
181 changes: 181 additions & 0 deletions docs/docs/reference/phantom-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
---
layout: doc-page
title: "Phantom Types"
---


What is a phantom type?
-----------------------

A phantom type is a manifestation of abstract type that has no effect on the runtime.
These are useful to prove static properties of the code using type evidences.
As they have no effect on the runtime they can be erased from the resulting code by
the compiler once it has shown the constraints hold.

When saying that a they have no effect on the runtime we do not only mean side effects
like IO, field mutation, exceptions and so on. We also imply that if a function receives
a phantom its result will not be affected by this argument.

As phantom do not live at runtime they cannot be subtypes of `scala.Any`, which deffines
methods such as `hashCode`, `equals`, `getClass`, `asInstanceOf` and `isInstanceOf`.
All these operations cannot exist on phantoms as there will not be an underlying object
instance at runtime. At first glace this could look like a limitation, but in fact not
having `asInstanceOf` will make constraints more reliable as it will not be possible to
downcast a phantom value to fake an evidence.

If they don't live in the universe bounded by `scala.Any` and `scala.Nothing` where do
they live? The answer is in their own type universes bounded by their phantom `Any` and `Nothing`.
In fact we allow multiple phantom universes to exist.

```none
+-----+ +---------------+ +--------------------+
| Any | | MyPhantom.Any | | MyOtherPhantom.Any |
+-----+ +---------------+ +--------------------+
| | |
+----+------+ +-----+------+ ...
| | | |
+--------+ +--------+ +------+ +--------+
| AnyRef | | AnyVal | | Inky | | Blinky |
+--------+ +--------+ +------+ +--------+
... ... | |
+------+ | +-------+ |
| Null | | | Pinky | |
+------+ | +-------+ |
| | | |
+------+-----+ +----+------+ ...
| | |
+---------+ +-------------------+ +------------------------+
| Nothing | | MyPhantom.Nothing | | MyOtherPhantom.Nothing |
+---------+ +-------------------+ +------------------------+
```

Inside a universe it types support the full Dotty type system. But we cannot mix types from
different universes with `&`, `|` or in type bounds. Each type must be fully defined one universe.


Implement your own phantom type
-------------------------------
Phantom types are definded by an `object` extending `scala.Phantom`. This object will represent
a universe of phantom types that is completely separated from types in `scala.Any` or other
phantom universes. We can define our phantom universe `MyPhantoms`.

```scala
object MyPhantoms extends Phantom
```

```scala
package scala
trait Phantom { // only an `object` can extend this trait
protected final type Any // not a subtype of scala.Any
protected final type Nothing // subtype of every subtype of this.Any
protected final def assume: this.Nothig
}
```

The types in the phantom universe are defined by the top type `Phantom.Any` and bottom type
`Phantom.Nothing`. This means that `MyPhantoms.Any` and `MyPhantoms.Nothing` are the bounds
of the phantom types in `MyPhantoms`, these bounds are `protected` and can not be accessed
from outside `MyPhantoms` unless an alias is defined for them.

New phantom types can be defined using `type XYZ <: OtherPhantom` (where `>: MyPhantom.Nothing`
will be inferred), this would be the equivalent of `class XYZ extends OtherClass` on a types
only (no runtime definitions). Or aliased with `type MyAny = OtherPhantom`. Whitin `MyPhantoms`
it is possible to refer to `MyPhantoms.Any` and `MyPhantoms.Nothing` with `this.Any` and
`this.Nothing` (or just `Any` and `Nothing` but not recommended). Using this we will define
four the four phantoms: `Inky`, `Blinky`, `Pinky` and `Clyde`.

```scala
object MyPhantoms extends Phantom {
type Inky <: this.Any
type Blinky <: this.Any
type Pinky <: Inky
type Clyde <: Pinky
}
```

Values of phantom type can be created using the `protected def assume`. This value can be
used as a value of this phantom type as it's type is `this.Nothig` (or `MyPhantoms.Nothing`).
Usually this value will be used to define a `implicit def` that returns the phantom with a more
precise type. In our example we will only create values of type `Pinky` and `Clyde`

```scala
object MyPhantoms extends Phantom {
... // Type definition

def pinky: Pinky = assume
def clyde: Clyde = assume
}
```

### Using the phantoms

From the user of the phantom type there is almost no difference, except for stronger type guarantees.
We can look at the following simple application:

```scala
import MyPhantoms._
object MyApp {
def run(phantom: Inky) = println("run")
def hide(phantom: Blinky) = println("run")

run(pinky)
run(clyde)
}
```

Note given the way we defined the phantoms it is impossible to call the `hide` as we did not
expose any value of type `Blinky` to the user. We cannot call `hide(MyPhantoms.assume)` as
`assume` is protected, `hide(null.asInstanceOf[Blinky])` does not compile because it is impossible
to cast to a phantom and `hide(throw new Exception)` (or `hide(???)`) does not compile as `throw` of
type `scala.Nothing` is not in the same type universe as `Blinky`. Good, we caught all possible
mistakes before when compiling the code, no surprises at runtime (hopefully not in production).


What happens with Phantoms at runtime?
--------------------------------------

Disclaimer: Most of phantom erasure is implemented, but not all of is has been merged in `dotty/master` yet.

As phantom have no effect on the result of a method invocation we just remove them for the call an definition.
The evaluation of the phantom parameter is still be done unless it can be optimized away.
By removing them we also restrict overloading as `def f()` and `def f(x: MyPhantom)` will
have the same signature in the bytecode, just use different names to avoid this.

At runtime the `scala.Phantom` trait will not exist.
* The object extending `Phantom` will not extend it anymore
* All phantom types will be erased on a single erased type (important in overloading for methods returning a phantom)
* Calls to `Phantom.assume` will become a reference to a singleton of the erased phantom type and will be removed wherever possible

```scala
object MyOtherPhantom extends Phantom {
type MyPhantom <: this.Any
def myPhantom: MyPhantom = assume

def f1(a: Int, b: MyPhantom, c: Int): Int = a + c

def f2 = {
f1(3, myPhantom, 2)
}
}
```

will be compiled to

```scala
object MyOtherPhantom {
def myPhantom(): <ErasedPhantom> = <ErasedPhantom.UNIT>

def f1(a: Int, c: Int): Int = a + c

def f2 = {
val a$ = 3
myPhantom()
val b$ = 3
f1(a$, b$)
}
}
```

Note that `myPhantom` is not removed as it could have some side effect before returning the phantom.
To remove it just use `inline def myPhantom` instead this will remove the call and allow the
`<ErasedPhantom.UNIT>` to be optimized away.
2 changes: 2 additions & 0 deletions docs/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ sidebar:
url: docs/reference/type-lambdas.html
- title: Implicit Function Types
url: docs/reference/implicit-function-types.html
- title: Phantom Types
url: docs/reference/phantom-types.html
- title: Enums
subsection:
- title: Enumerations
Expand Down
24 changes: 24 additions & 0 deletions tests/neg/reference-phantom-type-1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
object MyPhantoms extends Phantom {
type Inky <: this.Any
type Blinky <: this.Any
type Pinky <: Inky
type Clyde <: Pinky

def pinky: Pinky = assume
def clyde: Clyde = assume
}

import MyPhantoms._
object MyApp {
def run(phantom: Inky) = println("run")
def hide(phantom: Blinky) = println("run")

run(pinky)
run(clyde)

hide(pinky) // error
hide(clyde) // error
hide(MyPhantoms.assume) // error
hide(throw new Exception) // error
hide(???) // error
}
20 changes: 20 additions & 0 deletions tests/neg/reference-phantom-type-2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
object MyPhantoms extends Phantom {
type Inky <: this.Any
type Blinky <: this.Any
type Pinky <: Inky
type Clyde <: Pinky

def pinky: Pinky = assume
def clyde: Clyde = assume
}

import MyPhantoms._
object MyApp {
def run(phantom: Inky) = println("run")
def hide(phantom: Blinky) = println("run")

run(pinky)
run(clyde)

hide(null.asInstanceOf[Blinky]) // error
}
29 changes: 29 additions & 0 deletions tests/pos/reference/phantom-types.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
object MyPhantoms extends Phantom {
type Inky <: this.Any
type Blinky <: this.Any
type Pinky <: Inky
type Clyde <: Pinky

def pinky: Pinky = assume
def clyde: Clyde = assume
}

import MyPhantoms._
object MyApp {
def run(phantom: Inky) = println("run")
def hide(phantom: Blinky) = println("run")

run(pinky)
run(clyde)
}

object MyOtherPhantom extends Phantom {
type MyPhantom <: this.Any
def myPhantom: MyPhantom = assume

def f1(a: Int, b: MyPhantom, c: Int): Int = a + c

def f2 = {
f1(3, myPhantom, 2)
}
}