Blog

Der Vergleich von Äpfeln mit Birnen in Scala - oder Abstrakte Typen zur Rettung

Urs Peter

Urs Peter

Aktualisiert Oktober 22, 2025
6 Minuten

Abstrakte Typen in Scala können Ihr Leben sehr viel einfacher machen. In diesem Blog werde ich meine intellektuelle Reise rekapitulieren, auf der ich 'Äpfel mit Birnen' auf eine typsichere Weise vergleichen wollte, was mich zu abstrakten Typen führte.

Mein Ziel war es, einen Code zu schreiben, mit dem ich verschiedene Arten von Währungen so elegant wie möglich vergleichen kann. Was ich wollte, war eine sehr einfache DSL, mit der ich Folgendes tun konnte: [scala]2.Dollar > 1.Euro[/scala]

Der Anfang

Damit habe ich angefangen: [scala] trait Money extends Ordered[Money] { val unit:String val amount:Double def compare(that:Money) = if(amount > that.amount) 1 else -1 } case class Euro(amount: Double) with Money { val unit = "EUR" } case class Dollar(amount:Double) with Money { val unit = "USD" } [/scala] Sie sehen wahrscheinlich schon, auf welches Problem wir stoßen werden. Wenn wir die gleichen Währungen vergleichen wollen, funktioniert dieser Code einwandfrei. Wenn wir jedoch zwei verschiedene Währungen vergleichen, wird er zwar kompiliert, aber ein falsches Ergebnis liefern: [scala] //funktioniert wie erwartet assert(Euro(200) > Euro(100)) //kompiliert, liefert aber ein falsches Ergebnis assert(Euro(99) > Dollar(100)) [/scala] Meine Frage war also, wie man mit dieser Situation umgehen kann. Eine Eigenschaft Money, die den Vergleich durchführt, ist definitiv nicht der richtige Weg, da sie die Umrechnung von Währungen nicht berücksichtigt. Mit diesem Entwurf kann ich Euros mit Dollars vergleichen, wobei wir nur Beträge vergleichen.

Keine echte Option für das Problem

Eine Sekunde lang habe ich überlegt, ob ich die folgende Prüfung in die Money's compare-Methode einbauen soll: [scala]def compare(that:Money) = { require(unit == that.unit) if(amount > that.amount) 1 else -1 [/scala] } Es erübrigt sich zu sagen, dass dies nicht als praktikable Lösung angesehen werden kann. Insbesondere im Hinblick auf das reichhaltige Typsystem von Scala, das Ihnen helfen sollte, genau solche Probleme zu lösen. Wie kann man also damit umgehen?

Abstrakte Typen zur Rettung

Minuten später erinnerte ich mich an abstrakte Typen, die genau die Lösung boten, die ich gesucht hatte. Anstatt die Vergleichsmethode einen Typ von Money akzeptieren zu lassen, definiere ich einen abstrakten Typ namens Currency, der von Money abgeleitet ist. Dieser abstrakte Typ wird dann als Eingabe für die Vergleichsmethode verwendet. [scala]trait Money { type Currency <: Money val unit: String val amount: Double def compare(that:Currency):Int = if(amount > that.amount) 1 else -1 [/scala] } Als nächstes kann ich eine OrderedMoney-Eigenschaft definieren, um die Ordered-Eigenschaft einzuführen, für die die compare-Methode überhaupt erst benötigt wird. [scala] trait OrderedMoney[T <: Money] extends Money with Ordered[T] [/scala] Da der Super-Trait Money bereits eine Implementierung der Vergleichsmethode enthält, muss der Trait OrderedMoney diese nicht implementieren. Natürlich hätten wir die Implementierung der compare-Methode auch in der OrderedMoney-Eigenschaft bereitstellen können. Der Effekt ist jedoch derselbe. Die OrderedMoney-Eigenschaft muss für jede konkrete Währungsimplementierung wie folgt eingebunden werden: [scala] case class Euro(val amount: Double) extends OrderedMoney[Euro]{ type Currency = Euro val unit = "EUR" } case class Dollar(val amount: Double) extends OrderedMoney[Dollar]{ type Currency = Dollar val unit = "USD" } [/scala] Ab diesem Punkt stellt der Compiler sicher, dass ich nur Äpfel mit Äpfeln und nicht Äpfel mit Birnen vergleiche: [scala] //funktioniert wie erwartet assert(Euro(200) > Euro(100)) //kompiliert nicht mehr, wie vorgesehen assert(Dollar(100) > Euro(100)) scala: Abweichende implizite Konvertierung...[/scala]

So vergleichen Sie Äpfel mit Birnen

Schließlich möchten wir Äpfel mit Birnen oder hier Dollars mit Euros vergleichen. Um das zu erreichen, müssen wir lediglich eine implizite Umrechnungslogik hinzufügen und unser Geld-DSL ist (fast) vollständig: [scala]object Conversions { implicit def fromEuroToDollar(d: Euro) = new Dollar(d.amount 1.2) implicit def fromDollarToEuro(d: Dollar) = new Euro(d.amount 0.85) [/scala] } Der Einfachheit halber verwenden wir einen fest kodierten Wert, um von einer Währung in eine andere umzurechnen. Idealerweise müsste eine solche Funktionalität in einer separaten Klasse untergebracht werden, etwa in einem CurrencyConverter. Mit diesen Umrechnungen können nun endlich Äpfel mit Birnen verglichen werden: [scala]//funktioniert wie erwartet assert(Euro(200) > Euro(100)) //kompiliert UND funktioniert wie erwartet assert(Dollar(100) > Euro(100))[/scala]

Der letzte Schliff

Für einen Benutzer dieser API wäre es bequem, eine natürliche Syntax wie 2.euro anstelle von Euro(2) zu verwenden. Dazu muss nur ein weiteres einfaches Stück Umrechnungslogik hinzugefügt werden: [scala]object Conversions { ... implicit def fromDoubleToCurrency(d: Double) = new { def euro = Euro(d) def dollar = Dollar(d) } }[/scala] Diese implizite Methode wandelt einen Double in ein anonymes Objekt um, das eine Euro- und eine Dollar-Methode enthält. Damit ist jedes Double mit diesen Methoden gepimpt, so dass diese DSL so einfach zu benutzen ist, wie ursprünglich beabsichtigt: [scala]2.euro > 2.dollar[/scala]

Aufrunden: Hinzufügen von Grundrechenarten

Sind wir schon so weit? Nun, wir sind schon recht weit gekommen, aber es wäre schön, wenn wir mit Währungen rechnen könnten, vorzugsweise mit verschiedenen Arten von Währungen. Die folgenden Operationen wären sehr nützlich: [scala]// [/scala] 2.euro + 10.euro //Berechnungen mit unterschiedlichen Währungen 1.euro + 20.dollar - 5.pounds Die Frage ist, wie Sie diese Anforderung mit möglichst geringen Auswirkungen umsetzen können. Mit ein paar Zeilen Code kann diese zusätzliche Anforderung ganz einfach erfüllt werden: [scala] trait Money { ... def create(amount:Double):Currency def +(that:Currency) = create(amount + that.amount) def -(that:Currency) = create(amount - that.amount) [/scala] } Wir haben Money eine + und - Methode und eine abstrakte create Methode hinzugefügt, die benötigt wird, um eine Währung mit dem berechneten Betrag zu instanzieren. Für jeden Währungstyp müssen wir nur noch die create-Methode wie folgt implementieren: [scala]case class Euro(val Betrag: Double) extends OrderedMoney[Euro]{ type Währung = Euro val Einheit = "EUR" def create(Betrag:Double) = Euro(Betrag) [/scala] } Und von nun an können wir nicht nur verschiedene Währungen vergleichen, sondern auch mit ihnen rechnen: [scala]//Berechnungen und Vergleiche mit verschiedenen Arten von Währungen 1.euro + 20.euro > 15.dollar - 3.euro[/scala]

Alles auf einmal

Hier also der gesamte Code, mit dem Sie Äpfel mit Birnen vergleichen und einige grundlegende arithmetische Operationen durchführen können, und das alles auf eine typsichere Weise: [scala] trait Geld { Typ Währung <: Geld val unit: String val amount: Double def compare(that:Currency):Int = if(Betrag > der.Betrag) 1 sonst -1 def +(das:Währung) = create(Betrag + das.Betrag) def -(das:Währung) = create(Betrag - das.Betrag) protected def create(Betrag:Double):Currency } trait OrderedMoney[T <: Money] extends Money with Ordered[T] case class Euro(Betrag: Double) extends OrderedMoney[Euro]{ Typ Währung = Euro val unit = "EUR" def create(Betrag:Double) = Euro(Betrag) } case class Dollar(Betrag: Double) extends OrderedMoney[Dollar]{ Typ Währung = Dollar val unit = "USD" def create(Betrag:Double) = Dollar(Betrag) } Objekt Conversions { implicit def fromEuroToDollar(d: Euro) = Dollar(d.Betrag 1.2) implicit def fromDollarToEuro(d: Dollar) = Euro(d.Betrag 0.85) implicit def fromDoubleToCurrency(d: Double) = new { def euro = Euro(d) def dollar = Dollar(d) } } //Verwendungsbeispiele: assert(2.dollar + 3.euro >= 1.Dollar + 1.Dollar + 1.Euro) [/scala] Nachdem ich diesen Code geschrieben habe, habe ich endlich verstanden, warum Martin Oderskys neue Firma 'typesafe' getauft wurde: das ist es, worum es bei Scala zu einem 'großen Teil' geht ;-)

Danksagungen

Dieser Blog wurde teilweise durch ein Beispiel aus dem Buch Programming in Scala, Kapitel 20, Case study currencies inspiriert .

Verfasst von

Urs Peter

Contact

Let’s discuss how we can support your journey.