Phantom Types in Scala: Semantisch korrekter Code zur Compile Time

Scala Bytes: The Lion sleeps tonight

Heiko Seeberger

Das Konzept der Phantom Types dient dazu, schon zur Compile Time sicherzustellen, dass wir ein API semantisch korrekt verwenden. Dieser Artikel zeigt ein einfaches Scala-Beispiel.

Tiere können entweder wach sein oder schlafen. Wenn sie wach sind, können sie sich fortbewegen, essen etc. und wenn sie schlafen, können sie z. B. träumen. Andersherum funktioniert das nicht. Diesen Sachverhalt wollen wir so in Programmcode umsetzen, dass wir eine Methode nicht aufrufen können, wenn es keinen Sinn ergibt. Viele denken jetzt vermutlich an ein Feld asleep in der Klasse Animal und darauf aufbauend an die Überprüfung von Preconditions in den jeweiligen Methoden. Aber damit stellen wir die Korrektheit erst zur Laufzeit sicher, was dazu führt, dass IllegalStateExceptions o. ä. ausgelöst werden. Viel besser wäre es, wenn wir schon zur Compile Time auf mögliche Fehler hingewiesen würden. Eine Möglichkeit, wie wir das vermutlich in jeder beliebigen, objektorientierten Programmiersprache umsetzen könnten, wäre die Verwendung zweier unterschiedlicher Klassen. Also ein AwakeAnimal und ein AsleepAnimal, die beide nur die jeweils zulässigen Methoden implementieren. Allerdings bekommen wir dann Schwierigkeiten beim weiteren Vererben, z. B. bei der Einführung von Säugetieren, Vögeln etc. Aber es geht auch ohne Aufspalten der Klasse Animal, indem wir sie einfach mit Phantom Types parametrisieren. Das sind Typen, die niemals instanziiert, sondern „nur“ für Compile Time Constraints verwendet werden:

sealed trait AwakeOrAsleep
class Animal[A <: awakeorasleep="">

Hier definieren wir mit dem „Operator“ <: ein sog. Upper Bound für den Typparameter, d. h. wir legen fest, dass A ein Subtyp von AwakeOrAsleep sein muss. Natürlich brauchen wir nun noch die zwei Subtypen, um die sich unser Beispiel dreht:

sealed trait Awake extends AwakeOrAsleep
sealed trait Asleep extends AwakeOrAsleep

Wir machen alle Traits sealed, damit niemand konkrete Subklassen anlegen und diese dann instanziieren kann. Soweit so gut, aber was hilft uns das? Um das zu erkennen, ergänzen wir die Klasse Animal um die Methoden goToSleep und wakeUp:

def goToSleep = new Animal[Asleep]
def wakeUp = new Animal[Awake]

Offenbar geben diese korrekt parametrisierte Animals zurück, aber noch können wir beide Methoden aufrufen, unabhängig ob wach oder schlafend:

scala> new Animal[Awake].wakeUp
res0: Animal[Awake] = Animal@3d2fb7ef

Dabei wollen wir doch erreichen, dass genau das nicht funktioniert, also gar nicht erst kompiliert. Dazu müssen wir noch ein letztes Mal Hand anlegen, indem wir die beiden Methoden ebenfalls parametrisieren. Zunächst der Code und der Beweis, dass wir nun tatsächlich am Ziel sind:

def goToSleep[B >: A <: awake="" new="" animal="" def="" wakeup="">: A <: asleep="" new="" animal="">

Hier der Beweis:

scala> new Animal[Awake].wakeUp
:12: error: type arguments [Awake] do not conform to method wakeUp's type parameter bounds [B >: Awake <: asleep="">
Geschrieben von
Heiko Seeberger
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.