scalatutorial.de

Stufe 4Funktional denken · Lektion 22 von 34

Either, Left und Right

Aktualisiert am 24. September 20263 Min. LesezeitSchritt 4/6 dieser Stufe

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

 Scala 3
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.

Either als zwei Gleise: jeder Schritt kann auf das Fehlergleis wechselnRight – ErfolgsgleisLeft – Fehlergleisparseprüfenspeichern
Jeder Schritt läuft auf dem Erfolgsgleis weiter oder wechselt auf das Fehlergleis – und bleibt dort.

Ein eigener Fehlertyp

Die Stärke von Either kommt zur Geltung, wenn der Fehlertyp selbst etwas aussagt:

 Scala 3
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

 Scala 3
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:

 Scala 3
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

MethodeWirkung
map(f)wandelt den Right-Wert um
flatMap(f)hängt einen Schritt an, der selbst Either liefert
left.map(f) bzw. leftMap in Bibliothekenwandelt den Left-Wert um
getOrElse(x)Right-Wert oder Ersatz
fold(fl, fr)beide Fälle zu einem Ergebnis zusammenführen
toOptionSome bei Right, None bei Left
swaptauscht die Seiten
isRight / isLeftAbfrage der Seite

Umwandeln aus Option und Try

 Scala 3
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).toEither

toRight(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:

 Scala 3
val pruefungen = List(pruefeName(""), pruefeAlter("12"))
val fehler = pruefungen.collect { case Left(f) => f }
// List(NameLeer, ZuJung(12))

Passt dazu