Stufe 4Funktional denken · Lektion 22 von 34
Either, Left und Right
Either hat zwei Seiten: Right für das Ergebnis, Left für den Fehler. Anders als Option sagt es, warum etwas schiefging, und anders als Try kann der Fehler ein eigener, fachlicher Typ sein – der Compiler prüft, dass jeder Fall behandelt wird.
Zwei Seiten
val gut: Either[String, Int] = Right(42)
val schlecht: Either[String, Int] = Left("keine Zahl")Either[L, R] enthält entweder einen Wert vom Typ L (in Left) oder einen vom Typ R (in Right). Die Konvention: Right ist „richtig“, also das Ergebnis; Left ist der Fehler. Diese Rollenverteilung ist seit Scala 2.12 fest eingebaut – map und flatMap wirken auf die rechte Seite.
Ein eigener Fehlertyp
Die Stärke von Either kommt zur Geltung, wenn der Fehlertyp selbst etwas aussagt:
enum AnmeldeFehler:
case NameLeer
case NameZuLang(laenge: Int)
case AlterUngueltig(eingabe: String)
case ZuJung(alter: Int)
import AnmeldeFehler.*
case class Anmeldung(name: String, alter: Int)
def pruefeName(n: String): Either[AnmeldeFehler, String] =
if n.isBlank then Left(NameLeer)
else if n.length > 40 then Left(NameZuLang(n.length))
else Right(n.trim)
def pruefeAlter(s: String): Either[AnmeldeFehler, Int] =
s.toIntOption match
case None => Left(AlterUngueltig(s))
case Some(a) if a < 16 => Left(ZuJung(a))
case Some(a) => Right(a)Verketten im for-Ausdruck
def anmelden(name: String, alter: String): Either[AnmeldeFehler, Anmeldung] =
for
n <- pruefeName(name)
a <- pruefeAlter(alter)
yield Anmeldung(n, a)
anmelden("Ada", "36") // Right(Anmeldung(Ada,36))
anmelden("", "36") // Left(NameLeer)
anmelden("Ada", "zwölf") // Left(AlterUngueltig(zwölf))Der erste Fehler bricht die Kette ab – wie bei Option und Try. Der Unterschied: Das Ergebnis sagt genau, was nicht stimmte.
Fehler auswerten
Weil AnmeldeFehler ein enum ist, prüft der Compiler beim Pattern Matching, ob alle Fehlerarten behandelt werden:
def meldung(f: AnmeldeFehler): String = f match
case NameLeer => "Bitte geben Sie einen Namen an."
case NameZuLang(l) => s"Der Name ist $l Zeichen lang, erlaubt sind 40."
case AlterUngueltig(e) => s"„$e“ ist keine gültige Altersangabe."
case ZuJung(a) => s"Mit $a Jahren ist eine Anmeldung noch nicht möglich."
anmelden("Ada", "12") match
case Right(a) => println(s"Willkommen, ${a.name}")
case Left(f) => println(meldung(f))Kommt später eine neue Fehlerart hinzu, meldet der Compiler jede Stelle, an der sie noch nicht berücksichtigt ist. Das ist bei Exceptions nicht möglich.
Nützliche Methoden
| Methode | Wirkung |
|---|---|
map(f) | wandelt den Right-Wert um |
flatMap(f) | hängt einen Schritt an, der selbst Either liefert |
left.map(f) bzw. leftMap in Bibliotheken | wandelt den Left-Wert um |
getOrElse(x) | Right-Wert oder Ersatz |
fold(fl, fr) | beide Fälle zu einem Ergebnis zusammenführen |
toOption | Some bei Right, None bei Left |
swap | tauscht die Seiten |
isRight / isLeft | Abfrage der Seite |
Umwandeln aus Option und Try
val alterOpt: Option[Int] = "36".toIntOption
val alterEither: Either[String, Int] = alterOpt.toRight("keine Zahl")
import scala.util.Try
val versuch: Either[Throwable, Int] = Try("x".toInt).toEithertoRight(fehler) macht aus einer Option ein Either, wobei None zum angegebenen Left wird. Mit Either.cond(bedingung, rechts, links) entsteht ein Either direkt aus einer Bedingung.
Alle Fehler sammeln?
Die for-Verkettung bricht beim ersten Fehler ab. Bei einem Formular möchte man aber oft alle Fehler gleichzeitig melden. Dafür gibt es eigene Datentypen, etwa Validated in der Bibliothek Cats. Ohne Zusatzbibliothek lässt sich das für einfache Fälle so lösen:
val pruefungen = List(pruefeName(""), pruefeAlter("12"))
val fehler = pruefungen.collect { case Left(f) => f }
// List(NameLeer, ZuJung(12))