Grundlagen
Variablen & Typen
Bevorzugen Sie val (unveränderlich) über var (veränderlich) für sichereren, vorhersagbareren Code. Scala leitet Typen ab, aber explizite Annotationen helfen der Lesbarkeit für öffentliche APIs. Alles ist ein Objekt – keine Primitives (Int, Boolean sind Klassen).
val name = "Alice" // immutable (preferred)
var age = 30 // mutable
val pi: Double = 3.14159
val isDev: Boolean = true
val nums: List[Int] = List(1, 2, 3)
println(name.getClass) // class java.lang.StringString-Interpolation
s"..." aktiviert ${expr}-Interpolation. f"..." fügt printf-Style-Formatierung hinzu (%s, %.2f). raw"..." deaktiviert Escape-Sequenzen. Diese Präfixe machen String-Erstellung typsicher und lesbar.
val name = "Alice"
val age = 30
println(s"Name: ${name}, Age: ${age}")
println(s"Next year: ${age + 1}")
println(s"Upper: ${name.toUpperCase}")
println(f"${name}%s weighs ${65.5}%.1f kg") // formatted
println(raw"No \n escape") // raw stringTuples
Tuples gruppieren 2-22 heterogene Werte. Zugriff via _1, _2 (1-indiziert). Destrukturieren mit val (a, b) = tuple. Für mehr als 2 Elemente bevorzugen Sie Case-Klassen für benannte Felder und bessere Lesbarkeit.
val pair = ("Alice", 30)
println(pair._1) // Alice
println(pair._2) // 30
val (name, age) = pair // destructure
println(s"${name}: ${age}")
// Scala 3: val p = ("a", 1, 2.0)
val triple = ("a", 1, 2.0)
println(triple._3) // 2.0Typ-Inferenz & Ascription
Scala leitet Typen für lokale Variablen und Rückgabetypen ab. Verwenden Sie explizite Typen für öffentliche APIs, rekursive Funktionen und mehrdeutige Fälle. Type-Ascription (expr: Type) erzwingt einen Typ – nützlich für Upcasting oder Disambiguierung.
val x = 42 // Int inferred
val y: Long = 42 // explicit Long
val z = 42: Long // type ascription
val list = List(1, 2, 3) // List[Int]
val mixed: List[Any] = List(1, "a", true)
def double(x: Int) = x * 2 // return type inferredUnit & Nothing
Unit ist wie void (ein Wert: ()). Nothing ist der Bottom-Typ ohne Instanzen – verwendet für Funktionen, die nie zurückkehren (throw, Endlosschleife). Nothing ist ein Subtyp aller Typen und ermöglicht flexible Typ-Inferenz.
def printIt(x: Int): Unit = println(x) // like void
val u: Unit = () // Unit has one value: ()
// Nothing is the bottom type—no instances
def error(msg: String): Nothing =
throw new RuntimeException(msg)
// Nothing is a subtype of everything
val n: Nothing = error("boom")Strings
Häufige String-Methoden
Scala-Strings sind Java-Strings mit zusätzlichen Methoden via impliziter Konvertierungen (StringOps). Die meisten Methoden geben neue Strings zurück (unveränderlich). Verwenden Sie diese statt manueller Schleifen für Klarheit und Korrektheit.
val s = "Hello, World"
println(s.length) // 12
println(s.toUpperCase) // HELLO, WORLD
println(s.toLowerCase) // hello, world
println(s.split(", ")) // Array(Hello, World)
println(s.replace("o", "0")) // Hell0, W0rld
println(s.reverse) // dlroW ,olleH
println(s.contains("World")) // trueMehrzeilige Strings (Triple-Quote)
Triple-Quoted-Strings bewahren alle Whitespace und Zeilenumbrüche. Verwenden Sie stripMargin mit |, um Code sauber auszurichten – nur Text nach | wird behalten. Ideal für SQL, JSON oder Templates, eingebettet in Code.
val sql = """
SELECT * FROM users
WHERE age > 18
ORDER BY name
"""
println(sql.trim)
// StripMargin for clean indentation
val text = """|Hello
|World""".stripMargin
println(text) // Hello
WorldString-Erstellung
mkString verbindet Collections mit optionalem Präfix/Suffix – idiomatisch und effizient. Verwenden Sie StringBuilder zum Erstellen großer Strings in Schleifen. Vermeiden Sie wiederholte +-Verkettung in Schleifen (erstellt viele intermediäre Objekte).
val parts = List("apple", "banana", "cherry")
println(parts.mkString(", ")) // apple, banana, cherry
println(parts.mkString("[", ", ", "]")) // [apple, banana, cherry]
val sb = new StringBuilder
for (p <- parts) sb.append(p).append(" ")
println(sb.toString.trim)String zu Zahl
toInt/toDouble werfen bei ungültiger Eingabe. Verwenden Sie toIntOption (Scala 2.13+) für sicheres Parsing, das Option zurückgibt. Für Bulk-Parsing verwenden Sie Try oder Either, um Fehler funktional ohne Exceptions zu behandeln.
val n = "42".toInt // 42
val d = "3.14".toDouble // 3.14
val b = "true".toBoolean // true
val safe = "abc".toIntOption // Some(42) or None
// Handling errors
val result = try "x".toInt catch { case _ => 0 }
println(result) // 0Regex
.r konvertiert einen String zu einem Regex. findFirstIn gibt Option, findAllIn gibt einen Iterator zurück. Verwenden Sie in Pattern-Matching mit case email(e) => für Extraktion. Regex ist unter der Haube Javas Pattern.
import scala.util.matching.Regex
val email: Regex = "[\w.]+@[\w]+\.[a-z]+".r
val text = "Contact: [email protected]"
email.findFirstIn(text) match {
case Some(e) => println(s"Found: ${e}")
case None => println("No email")
}
val replaced = "[0-9]+".r.replaceAllIn("a1b2c3", "#")
println(replaced) // a#b#c#Datenstrukturen
List & Seq
List ist eine unveränderliche singly-linked list – O(1) head/prepend, O(n) Random-Access. Verwenden Sie Vector für Random-Access (O(1) effektiv). +: prepended, :+ appended. Bevorzugen Sie unveränderliche Collections für Thread-Sicherheit.
val nums = List(1, 2, 3, 4, 5)
println(nums.head) // 1
println(nums.tail) // List(2,3,4,5)
println(nums.reverse) // List(5,4,3,2,1)
println(nums.take(2)) // List(1, 2)
println(nums.drop(2)) // List(3, 4, 5)
println(nums.mkString) // 12345
val combined = 0 +: nums :+ 6 // List(0,1,2,3,4,5,6)Map
Map ist unveränderlich – Operationen geben neue Maps zurück. Verwenden Sie get(key) für Option-Zugriff, getOrElse für Standardwerte. + fügt hinzu/aktualisiert, - entfernt. Für veränderliche Maps verwenden Sie scala.collection.mutable.Map. Schlüssel müssen Hashable sein.
val ages = Map("Alice" -> 30, "Bob" -> 25)
println(ages("Alice")) // 30 (throws if missing)
println(ages.getOrElse("Eve", 0)) // 0 (safe)
val updated = ages + ("Eve" -> 28) // new Map
val removed = ages - "Bob"
ages.foreach { case (k, v) => println(s"${k}: ${v}") }
println(ages.keys) // Set(Alice, Bob)Set
Set ist unveränderlich mit O(1) contains. union (|), intersect (&), diff (~) für Mengenoperationen. + fügt hinzu, - entfernt. Verwenden Sie für Deduplizierung und Mitgliedschaftstests. Veränderliche Variante: scala.collection.mutable.Set.
val a = Set(1, 2, 3)
val b = Set(3, 4, 5)
println(a union b) // Set(1,2,3,4,5)
println(a intersect b) // Set(3)
println(a diff b) // Set(1,2)
println(a subsetOf(Set(1,2,3,4))) // true
val added = a + 6 // Set(1,2,3,6)Option (Null-Sicherheit)
Option ersetzt null – Some(value) oder None. Verwenden Sie map/filter/flatMap für Transformationen, getOrElse für Standardwerte. For-Comprehensions funktionieren auf Option. Dies eliminiert NullPointerException, indem Abwesenheit explizit im Typ gemacht wird.
def findUser(id: Int): Option[String] =
if (id == 1) Some("Alice") else None
val name = findUser(1)
println(name.getOrElse("Unknown")) // Alice
println(name.map(_.toUpperCase)) // Some(ALICE)
println(name.filter(_.startsWith("A"))) // Some(Alice)
val result = for {
n <- findUser(1)
if n.startsWith("A")
} yield n.toUpperCase // Some(ALICE)Array & Vector
Array ist ein veränderliches Java-Array (am schnellsten, aber keine funktionalen Updates). Vector ist unveränderlich mit effektiv O(1) Random-Access und Updates – bevorzugt für unveränderliche Random-Access-Collections. Verwenden Sie List für sequenziell, Vector für indiziert.
val arr = Array(1, 2, 3, 4) // mutable, Java array
arr(0) = 10
println(arr(0)) // 10
val vec = Vector(1, 2, 3, 4) // immutable, fast random access
println(vec(2)) // 3
val updated = vec.updated(0, 10) // Vector(10,2,3,4)
// Vector: O(1) random access + immutable
// Array: mutable, Java interop, fastestKontrollfluss
If / Else (Ausdruck)
In Scala ist if/else ein Ausdruck, der einen Wert zurückgibt. Dies eliminiert die Notwendigkeit eines ternären Operators. Beide Zweige müssen kompatible Typen haben. Verwenden Sie dies für prägnante bedingte Zuweisung.
val score = 85
val grade =
if (score >= 90) "A"
else if (score >= 80) "B"
else if (score >= 70) "C"
else "F"
println(grade) // B
// if returns a value—no ternary operator neededFor-Comprehension
for-Comprehensions iterieren und können filtern (if-Guards) und transformieren (yield). Ohne yield ist es eine Schleife; mit yield baut es eine Collection. to inkludiert das Ende, until schließt es aus. Äquivalent zu flatMap/map/filter-Ketten.
for (i <- 1 to 5) println(i) // 1 2 3 4 5
for (i <- 1 until 5) print(i) // 1 2 3 4
// With yield (generates collection)
val doubled = for (n <- List(1,2,3)) yield n * 2
println(doubled) // List(2, 4, 6)
// With filter (guard)
val evens = for (n <- 1 to 10 if n % 2 == 0) yield n
println(evens.toList) // List(2,4,6,8,10)Match (Pattern Matching)
match ist Scalas mächtiges Pattern-Matching – wie switch on steroids. Unterstützt Literale, OR (|), Guards (if), Typ-Matching und Destrukturierung. Muss erschöpfend sein (Compiler warnt bei fehlenden Fällen). Es ist ein Ausdruck, der einen Wert zurückgibt.
val n = 2
val label = n match {
case 0 => "zero"
case 1 | 2 | 3 => "small"
case x if x < 10 => "medium"
case _ => "large"
}
println(label) // small
// Match on types
def describe(x: Any): String = x match {
case i: Int => s"Int: ${i}"
case s: String => s"String: ${s}"
case _ => "unknown"
}While & Do-While
while/do-while sind imperative Schleifen, die Unit zurückgeben. Sie erfordern veränderlichen Zustand (var). Bevorzugen Sie For-Comprehensions oder Rekursion für idiomatisches funktionales Scala. Verwenden Sie while nur, wenn Performance es verlangt oder für Nebeneffekte.
var i = 0
while (i < 3) {
println(i)
i += 1
}
var j = 0
do {
println(j)
j += 1
} while (j < 3)
// Prefer recursion or for-comprehensions for immutabilityTry / Catch / Finally
try/catch/finally ist wie Java, aber verwendet Pattern-Matching in catch. Bevorzugen Sie Try für funktionale Fehlerbehandlung – es wickelt Exceptions als Success/Failure-Werte und ermöglicht map/flatMap-Ketten ohne try/catch-Boilerplate.
import scala.util.{Try, Success, Failure}
val result = try {
"abc".toInt
} catch {
case e: NumberFormatException => 0
} finally {
println("cleanup")
}
println(result) // 0
// Functional alternative
val r2 = Try("abc".toInt).getOrElse(0)
println(r2) // 0Funktionen
Methodendefinition
Methoden verwenden def name(params): ReturnType = body. Default-Params und benannte Argumente unterstützt. Unit-Rückgabe = nur Nebeneffekt. Einzeilige Bodys lassen Klammern weg. = ist erforderlich (ohne ist es eine Prozedur, die Unit zurückgibt).
def add(a: Int, b: Int): Int = a + b
def greet(name: String, greeting: String = "Hello"): String =
s"${greeting}, ${name}!"
def log(msg: String): Unit = println(msg)
println(add(3, 4)) // 7
println(greet("Alice")) // Hello, Alice!
println(greet("Bob", greeting = "Hi")) // named argLambda (anonyme Funktion)
Lambdas: (params) => body. Verwenden Sie _ als Shorthand für einzelne Parameter (x => x * 2 wird _ * 2). Mehrere _'s referenzieren verschiedene Parameter (_ + _). Lambdas sind First-Class – geben Sie sie an map, filter, reduce usw. weiter.
val square = (x: Int) => x * x
println(square(5)) // 25
val nums = List(1, 2, 3)
println(nums.map(_ * 2)) // List(2, 4, 6)
println(nums.filter(_ > 1)) // List(2, 3)
println(nums.reduce(_ + _)) // 6
// _ is shorthand for the parameterHigher-Order Functions
Higher-Order Functions nehmen oder geben Funktionen zurück. Dies ermöglicht mächtige Abstraktionen: map/filter/reduce, Komposition, partielle Anwendung. makeAdder gibt eine Closure zurück, die n erfasst. Dies ist das Herz der funktionalen Programmierung.
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
println(applyTwice(_ + 3, 5)) // 11
def makeAdder(n: Int): Int => Int = _ + n
val add5 = makeAdder(5)
println(add5(10)) // 15
// Functions returning functions = currying-likeCurrying & partielle Anwendung
Currying teilt Parameter in mehrere Listen: def f(a)(b). Partielles Anwenden mit _ erstellt spezialisierte Funktionen. Mehrere Parameterlisten verbessern Typ-Inferenz (der Compiler kann fs Typen aus der Liste ableiten). Häufig in Collection-APIs.
def add(a: Int)(b: Int): Int = a + b // curried
val add5 = add(5)_ // partially applied
println(add5(3)) // 8
// Multiple parameter lists
def foldLeft[A, B](list: List[A])(z: B)(f: (B, A) => B): B = ???
// Helps type inference
val sum = List(1,2,3).foldLeft(0)(_ + _)By-Name & Lazy
By-Name-Parameter (=> T) werden verzögert bei jeder Verwendung ausgewertet – nützlich für Logging (teure msg überspringen, wenn deaktiviert) und benutzerdefinierte Kontrollstrukturen. lazy val verzögert Initialisierung bis zum ersten Zugriff – verwenden Sie für teure oder optionale Werte.
// By-name parameter: evaluated on each use
def debug(msg: => String): Unit =
if (debugEnabled) println(msg)
// Lazy evaluation
lazy val expensive = computeHeavy()
println(expensive) // computed now
def computeHeavy(): Int = { println("computing"); 42 }Klassen & OOP
Klasse & Konstruktor
Primary-Constructor-Parameter sind in der Klassen-Signatur. val-Parameter werden unveränderliche Felder (öffentlicher Getter), var veränderlich. Der Klassen-Body IST der Konstruktor. Hilfskonstruktoren verwenden def this(...) und müssen einen anderen Konstruktor aufrufen.
class Person(val name: String, val age: Int) {
// Constructor params with val/var become fields
def greet: String = s"Hi, I'm ${name}"
def isAdult: Boolean = age >= 18
}
val p = new Person("Alice", 30)
println(p.greet) // Hi, I'm Alice
println(p.name) // Alice (val field)
println(p.isAdult) // trueCase-Klasse
Case-Klassen sind unveränderliche Datenklassen mit auto-generiertem equals, hashCode, toString, copy und Companion-Objekt mit apply/unapply. Verwenden Sie für Datenmodellierung und Pattern-Matching. Kein 'new' nötig. Sie sind die Grundlage von ADTs in Scala.
case class Point(x: Int, y: Int)
val p1 = Point(3, 4) // no 'new' needed
val p2 = Point(3, 4)
println(p1 == p2) // true (value equality)
val moved = p1.copy(x = 5) // Point(5, 4)
println(p1.x, p1.y) // 3 4 (fields auto-visible)
// Auto: equals, hashCode, toString, copy, companionTraits (Interfaces mit Implementierung)
Traits sind wie Java-Interfaces, können aber Implementierung haben. Eine Klasse kann mehrere Traits einmischen (mit extends/with). Traits ermöglichen mehrfache Vererbung von Verhalten. Verwenden Sie für geteilte Interfaces, Mixins und stackable Modifications via Linearization.
trait Greetable {
def name: String // abstract
def greet: String = s"Hello, ${name}" // concrete
}
trait Named {
val name: String
}
class User(val name: String) extends Greetable
val u = new User("Alice")
println(u.greet) // Hello, Alice
// Stackable traits via linearizationObject (Singleton)
object deklariert ein Singleton (eine Instanz). Companion-Objekte (gleicher Name wie eine Klasse) halten static-ähnliche Methoden, Factories (apply) und Extraktoren (unapply). Verwenden Sie apply für Factory-Methoden, damit Aufrufer 'new' weglassen. Dies ist idiomatisches Scala.
object Config {
val version = "1.0"
def load(): Map[String, String] = Map("key" -> "value")
}
println(Config.version) // 1.0
// Companion object (same name as class)
class Person(val name: String)
object Person {
def apply(name: String): Person = new Person(name)
}
val p = Person("Alice") // uses apply, no 'new'Vererbung & abstrakte Klasse
abstract class kann unimplementierte Member haben. Verwenden Sie extends zum Erben, override zum Neudefinieren. Bevorzugen Sie Traits für Mixins (mehrfache Vererbung). Verwenden Sie abstract class, wenn Sie Konstruktor-Parameter brauchen oder einen Basistyp wollen. Einfache Vererbung von Klassen.
abstract class Shape {
def area: Double // abstract
def describe: String = s"Area: ${area}"
}
class Circle(r: Double) extends Shape {
def area: Double = math.Pi * r * r
}
val c = new Circle(5)
println(c.describe) // Area: 78.53...
// override required for concrete members
// abstract class vs trait: use abstract for base, trait for mixinsCollections & Funktional
Map / Filter / Fold
map transformiert, filter wählt aus, reduce/foldLeft aggregieren. Diese sind der Kern der funktionalen Datentransformation. foldLeft nimmt einen Seed und ist assoziativ; reduce erfordert nicht-leer. Verwenden Sie diese statt Schleifen für Klarheit und Unveränderlichkeit.
val nums = List(1, 2, 3, 4, 5)
println(nums.map(_ * 2)) // List(2,4,6,8,10)
println(nums.filter(_ % 2 == 0)) // List(2,4)
println(nums.reduce(_ + _)) // 15
println(nums.foldLeft(0)(_ + _)) // 15
println(nums.sum) // 15
println(nums.mkString(", ")) // 1, 2, 3, 4, 5FlatMap & For-Comprehension
flatMap mappt und flacht in einem Schritt ab – essenziell für verschachtelte Collections und monadische Operationen. For-Comprehensions sind syntaktischer Zucker für flatMap/map/filter-Ketten. Verwenden Sie für komplexe verschachtelte Transformationen – es ist lesbarer.
val nested = List(List(1, 2), List(3, 4))
println(nested.flatten) // List(1,2,3,4)
println(nested.flatMap(_.map(_ * 2))) // List(2,4,6,8)
// Equivalent for-comprehension:
val result = for {
inner <- nested
n <- inner
} yield n * 2
println(result) // List(2,4,6,8)Gruppierung & Sortierung
groupBy partitioniert nach einem Schlüssel in eine Map. sorted sortiert natürlich, sortBy nach einer Schlüsselfunktion, sortWith mit einem Komparator. Diese geben neue Collections zurück (unveränderlich). Verwenden Sie für Datenanalyse, Kategorisierung und Ordnung.
val words = List("apple", "bat", "cat", "ant")
val byFirst = words.groupBy(_.head)
// Map(a -> List(apple, ant), b -> List(bat), c -> List(cat))
println(byFirst)
val sorted = words.sorted // List(ant, apple, bat, cat)
val byLen = words.sortBy(_.length) // List(ant, bat, cat, apple)
val desc = words.sortWith(_ > _) // descending
println(sorted, byLen)Either & Try
Try wickelt Exceptions als Success/Failure-Werte. Either repräsentiert Left (Fehler) oder Right (Erfolg) – verwenden Sie für Domänenfehler, wo Sie Fehlertypen unterscheiden wollen. Beide unterstützen map/flatMap für funktionale Fehler-Propagation.
import scala.util.{Try, Success, Failure}
def parse(s: String): Try[Int] = Try(s.toInt)
parse("42") match {
case Success(n) => println(s"OK: ${n}")
case Failure(e) => println(s"Err: ${e.getMessage}")
}
// Either for domain errors
def divide(a: Int, b: Int): Either[String, Int] =
if (b == 0) Left("div by zero") else Right(a / b)
println(divide(10, 2)) // Right(5)
println(divide(10, 0)) // Left(div by zero)Verzögerte Collections (View & Lazy)
.view macht Collections verzögert – Operationen werden bis zum Forcen (toList, sum usw.) aufgeschoben. Vermeidet intermediäre Collections für bessere Performance bei großen Daten. LazyList (früher Stream) ermöglicht unendliche Sequenzen – Elemente bei Bedarf berechnet.
val nums = (1 to 1000000).view
val result = nums
.filter(_ % 2 == 0)
.map(_ * 2)
.take(5)
.toList // forces evaluation
println(result) // List(4, 8, 12, 16, 20)
// view = lazy, no intermediate collections
// LazyList (Stream) for infinite sequences
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)Pattern Matching
Case-Klassen matchen
Sealed Traits + Case-Klassen bilden Algebraic Data Types (ADTs). Der Compiler prüft Erschöpftheit – fügen Sie einen Case hinzu und er warnt, wo Matches aktualisiert werden müssen. Pattern-Matching destrukturiert Case-Klassen direkt. Dies ist idiomatische Scala-Modellierung.
sealed trait Shape
case class Circle(r: Double) extends Shape
case class Square(s: Double) extends Shape
case class Rect(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
case Circle(r) => math.Pi * r * r
case Square(s) => s * s
case Rect(w, h) => w * h
}
println(area(Circle(5))) // 78.53...Guards & Bedingungen
Guards (if condition) fügen Patterns Laufzeit-Prüfungen hinzu. Sie machen Matching ausdrucksstärker. Reihenfolge ist wichtig – erster Match gewinnt. Verwenden Sie Guards für Bereiche, Bedingungen oder komplexe Logik, die einfache Patterns nicht ausdrücken können.
val n = 15
val desc = n match {
case x if x < 0 => "negative"
case 0 => "zero"
case x if x % 2 == 0 => "even"
case _ => "odd"
}
println(desc) // odd
// Guards add boolean conditions to patternsCollections matchen
:: cons-Pattern destrukturiert Listen in Head und Tail. _* matcht null oder mehr Elemente in Arrays/Listen. Diese Patterns ermöglichen rekursive Listenverarbeitung und strukturelle Dekomposition. Mächtig für Parsing und Baum-Traversierung.
val list = List(1, 2, 3, 4)
list match {
case Nil => println("empty")
case head :: Nil => println(s"one: ${head}")
case head :: tail => println(s"head=${head}, rest=${tail}")
case _ => println("other")
}
// head :: tail destructures a list
val arr = Array(1, 2, 3)
arr match {
case Array(1, _*) => println("starts with 1")
case _ => println("other")
}Option & Either matchen
Pattern-Matching auf Option/Either ist idiomatisch – Some(x)/None, Right(x)/Left(e). For-Comprehensions desugar zu flatMap mit match. Dies macht Fehlerbehandlung natürlich fließen ohne explizite if-else-Prüfungen.
def find(id: Int): Option[String] =
if (id == 1) Some("Alice") else None
find(1) match {
case Some(name) => println(s"Found: ${name}")
case None => println("Not found")
}
// In for-comprehensions
val result = for {
name <- find(1)
upper = name.toUpperCase
} yield upper
println(result) // Some(ALICE)Extraktoren (unapply)
Benutzerdefinierte Extraktoren via unapply ermöglichen Pattern-Matching auf jedem Typ. Die unapply-Methode gibt Option der extrahierten Werte zurück. Dies lässt Sie Ihre eigenen Patterns definieren – mächtig für DSLs und Parsing. Case-Klassen auto-generieren unapply.
object Email {
def unapply(s: String): Option[(String, String)] = {
val parts = s.split("@")
if (parts.length == 2) Some((parts(0), parts(1))) else None
}
}
"[email protected]" match {
case Email(user, domain) =>
println(s"User: ${user}, Domain: ${domain}")
case _ => println("Not an email")
}Generics & Implicits
Generische Klassen & Methoden
Generics (Typ-Parameter [A]) schreiben typsicheren, wiederverwendbaren Code. Verwenden Sie [A] für einen einzelnen Typ, [A, B] für zwei. Typ-Inferenz findet meist den Typ heraus. Generics werden zur Laufzeit gelöscht (JVM-Limitierung), aber zur Kompilierzeit geprüft.
class Stack[A] {
private var items: List[A] = Nil
def push(x: A): Unit = { items = x :: items }
def pop: Option[A] = items.headOption
}
val s = new Stack[Int]
s.push(1); s.push(2)
println(s.pop) // Some(2)
def first[A](list: List[A]): Option[A] = list.headOption
println(first(List("a", "b"))) // Some(a)Typ-Bounds
<: Upper Bound (A ist ein Subtyp), >: Lower Bound (A ist ein Supertyp). Context Bounds (A: Ordering) erfordern einen impliziten Wert dieses Typs. View Bounds (A <% B) sind deprecated – verwenden Sie Context Bounds stattdessen. Diese schränken Typ-Parameter ein.
// Upper bound: A must be Animal or subclass
class Box[A <: Animal](val content: A)
// Lower bound: A must be Dog or superclass
class Kennel[A >: Dog](val occupant: A)
// Context bound: A must have an Ordering
def max[A: Ordering](a: A, b: A): A =
if (implicitly[Ordering[A]].gt(a, b)) a else b
abstract class Animal { def name: String }
class Dog extends Animal { def name = "Rex" }Implizite Parameter
Implizite Parameter werden vom Compiler aus dem Scope injiziert. Verwenden Sie für Konfiguration, Type Classes oder Abhängigkeiten, die Sie nicht überall weitergeben wollen. Deklarieren Sie mit implicit val/def. Der Compiler durchsucht den umschließenden Scope und Companion-Objekte.
def greet(name: String)(implicit greeting: String): String =
s"${greeting}, ${name}!"
implicit val defaultGreeting: String = "Hello"
println(greet("Alice")) // Hello, Alice! (implicit injected)
println(greet("Bob")("Hi")) // Hi, Bob! (explicit override)
// Compiler finds implicit in scopeType Classes (implizite Konvertierungen)
Type Classes (via Implicits) fügen Typen Verhalten hinzu, ohne sie zu modifizieren – ad-hoc-Polymorphismus. Definieren Sie ein Trait, stellen Sie implizite Instanzen bereit und verwenden Sie implizite Parameter. So funktionieren Ordering, Numeric und Show. Flexibler als Vererbung.
trait Show[A] { def show(a: A): String }
object Show {
implicit val intShow: Show[Int] = (a: Int) => a.toString
implicit val strShow: Show[String] = identity
}
def printIt[A](a: A)(implicit s: Show[A]): Unit =
println(s.show(a))
printIt(42) // 42
printIt("hello") // hello
// Type class: ad-hoc polymorphismErweiterungsmethoden (Scala 2)
Implizite Klassen fügen vorhandenen Typen Erweiterungsmethoden hinzu. Definieren Sie implicit class mit einem einzelnen Parameter, und ihre Methoden werden auf diesem Typ verfügbar. Verwenden Sie, um Int, String usw. Utility-Methoden hinzuzufügen. Scala 3 verwendet die sauberere 'extension'-Syntax.
implicit class IntOps(val n: Int) extends AnyVal {
def times(f: => Unit): Unit = (1 to n).foreach(_ => f)
def squared: Int = n * n
}
5.times { print("hi") } // hihihihihi
println(5.squared) // 25
// Scala 3: extension (n: Int) def squared = n * nNebenläufigkeit & Future
Future & Async
Future repräsentiert eine asynchrone Berechnung. onComplete registriert einen Callback. Blockieren Sie nie (Await.result) in Produktions-Webservern – es bindet Threads. Verwenden Sie For-Comprehensions, um Futures funktional zu verketten. Erfordert einen ExecutionContext.
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f: Future[Int] = Future {
Thread.sleep(1000)
42
}
f.onComplete {
case scala.util.Success(v) => println(s"Got ${v}")
case scala.util.Failure(e) => println(s"Err: ${e}")
}
// Don't block in production—use callbacks or for-comprehensionsFutures komponieren
For-Comprehensions auf Futures führen sie sequenziell aus (jede awaitet die vorherige). Für parallele Ausführung starten Sie alle Futures zuerst, dann verwenden Sie Future.sequence zum Kombinieren. Future.traverse mappt + sequenziert in einem Schritt. Dies ist der idiomatische Weg, Async-Arbeit zu komponieren.
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f1 = Future { 10 }
val f2 = Future { 20 }
val sum = for {
a <- f1
b <- f2
} yield a + b // Future(30)
// Parallel execution
val results = Future.sequence(List(
Future { 1 }, Future { 2 }, Future { 3 }
))
results.map(_.sum) // Future(6)Parallele Collections
.par konvertiert eine Collection zu einer parallelen Version – Operationen verwenden automatisch mehrere Threads. Gut für CPU-gebundene Arbeit auf großen Collections. Vorsicht: nicht-assoziative Operationen (wie Subtraktion) können unterschiedliche Ergebnisse geben. Nicht für I/O-gebundene Arbeit.
val nums = (1 to 1000000).toList
val sum = nums.par.sum // parallel sum
println(sum)
val doubled = nums.par.map(_ * 2).toList
// .par converts to ParCollection
// Operations run on multiple threads
// Use for CPU-bound work on large collectionsPromise (manuelles Future)
Promise ist die schreibbare Seite eines Futures – Sie vervollständigen es manuell mit success/failure. Verwenden Sie beim Bridgen von Callback-basierten APIs zu Futures oder wenn Sie ein Future von mehreren Stellen aus vervollständigen müssen. Future ist read-only; Promise ist write-once.
import scala.concurrent.{Promise, Future, ExecutionContext}
import ExecutionContext.Implicits.global
val p = Promise[Int]()
val f = p.future
// Complete the promise from another thread
Future { Thread.sleep(100); p.success(42) }
f.foreach(println) // 42 (when complete)
// p.failure(new Exception) for errors
// Promise = write side, Future = read sideSync vs. Async (Await)
Await.result blockiert den aktuellen Thread, bis das Future vervollständigt (mit Timeout). Verwenden Sie nur in Tests oder Main-Methoden – Blockieren in Async-Code widerlegt den Zweck. In Produktion verwenden Sie Callbacks (onComplete, map) oder For-Comprehensions, um nicht-blockierend zu bleiben.
import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import ExecutionContext.Implicits.global
val f = Future { Thread.sleep(500); 42 }
// Block and wait (use sparingly—mainly in tests)
val result = Await.result(f, 1.second)
println(result) // 42
// Await.ready returns Try, Await.result returns value
// Avoid in production servers—use callbacks insteadImplicits Deep Dive
Implizite Parameter
Implizite Parameter werden vom Compiler automatisch weitergegeben, wenn ein impliziter Wert des matchenden Typs im Scope ist. Dies reduziert Boilerplate für 'Kontext'-Parameter (ExecutionContext, Logging, Konfiguration). Der Compiler durchsucht: lokaler Scope, Companion-Objekte, impliziter Scope. Sie können immer explizit weitergeben, um zu überschreiben. Übergebrauch macht Code schwer zu verfolgen – verwenden Sie für genuine kontextuelle Abhängigkeiten.
import scala.concurrent.ExecutionContext
// Method with implicit parameter
def process[A](data: List[A])
(implicit ec: ExecutionContext): Future[Unit] = {
Future { data.foreach(println) }
}
// The compiler finds an implicit ExecutionContext in scope
implicit val ec: ExecutionContext = ExecutionContext.global
process(List(1, 2, 3)) // ec passed automatically
// Explicitly providing (overrides implicit)
process(List(1, 2, 3))(myCustomEC)
// Multiple implicit parameters
def log(msg: String)(implicit
level: Level, logger: Logger): Unit = {
logger.log(level, msg)
}Implizite Konvertierungen
Implizite Konvertierungen konvertieren automatisch zwischen Typen, wenn nötig. implicit class (erbt AnyVal für Zero-Overhead) fügt vorhandenen Typen Erweiterungsmethoden hinzu – so fügt Scala Int, String usw. Methoden hinzu. Vorsicht: implizite Konvertierungen können Code verwirrend machen (was wird konvertiert?). Bevorzugen Sie implizite Klassen für Erweiterungen über rohe implicit defs. Aktivieren Sie mit import scala.language.implicitConversions.
import scala.language.implicitConversions
// Implicit conversion: one type to another
implicit def intToString(n: Int): String = n.toString
val s: String = 42 // intToString(42) called implicitly
// Extension via implicit class (Scala 2.10+)
implicit class RichInt(val self: Int) extends AnyVal {
def times(f: => Unit): Unit = (1 to self).foreach(_ => f)
def squared: Int = self * self
}
5.times { println("hi") } // prints hi 5 times
3.squared // 9
// Implicit conversion for type compatibility
implicit def javaToScalaList(jl: java.util.List[Int]): List[Int] =
import scala.jdk.CollectionConverters._
jl.asScala.toList
val javaList: java.util.List[Int] = ???
val scalaList: List[Int] = javaList // convertedImplizite Auflösungs-Priorität
Der Compiler löst Implicits nach Priorität: lokaler Scope > Companion-Objekte > importiert > geerbt. Wenn zwei Implicits desselben Typs gleich im Scope sind, erhalten Sie einen 'ambiguous implicit'-Fehler. Das LowPriorityImplicits-Trait-Pattern bietet Defaults, die von spezifischeren Implicits überschrieben werden können. Auflösungsreihenfolge zu verstehen ist entscheidend für Bibliotheksdesign – platzieren Sie Defaults in Low-Priority-Traits, damit Nutzer überschreiben können.
// Priority of implicit resolution (highest to lowest):
// 1. Local implicit (defined in current scope)
implicit val ec1: ExecutionContext = ec1
// 2. Implicit in companion object
object MyService {
implicit val ec2: ExecutionContext = ec2 // lower priority
}
// 3. Implicit scope (imported)
import somePackage.Implicits._
// 4. Implicit parameter default (inherited trait)
trait DefaultEc {
implicit val ec: ExecutionContext = ExecutionContext.global
}
// More specific type wins
implicit def ord1: Ordering[Int] = ???
implicit def ord2: Ordering[Int] = ??? // ambiguous error!
// LowPriorityImplicits trait pattern
object MyLib {
implicit val high: Ordering[Int] = ???
}
object MyLib extends LowPriorityImplicits
trait LowPriorityImplicits {
implicit val low: Ordering[Int] = ??? // fallback
}Context Bounds und Evidence
Context Bounds [A: TypeClass] sind syntaktischer Zucker für implizite Parameter – sie behaupten, dass ein implizites TypeClass[A] existiert. Verwenden Sie implicitly[TypeClass[A]] (Scala 2) oder summon[TypeClass[A]] (Scala 3) zum Abrufen. Context Bounds machen Type-Class-Constraints lesbar: def sort[A: Ordering]. Mehrere Bounds stapeln: [A: Ordering: Numeric]. Dies ist der idiomatische Weg, Type-Class-Anforderungen auszudrücken.
// Context bound: [A: Ordering] means there's an implicit Ordering[A]
def max[A: Ordering](a: A, b: A): A = {
val ord = implicitly[Ordering[A]] // retrieve the implicit
if (ord.gt(a, b)) a else b
}
// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
if (ord.gt(a, b)) a else b
// summon (Scala 3) instead of implicitly
def max3[A: Ordering](a: A, b: A): A = {
val ord = summon[Ordering[A]]
if (ord.gt(a, b)) a else b
}
// Type class evidence
def sort[A: Ordering](list: List[A]): List[A] =
list.sorted // uses the implicit Ordering
// Multiple context bounds
def process[A: Ordering: Numeric](x: A, y: A): A = ???Impliziter Scope und Companion-Objekte
Impliziter Scope ist breiter als nur der aktuelle Scope – er inkludiert Companion-Objekte der involvierten Typen. Deshalb müssen Sie Ordering[Int] nicht importieren: es lebt in Ints Companion. Dieser Mechanismus macht Type Classes ergonomisch: definieren Sie die Instanz im Companion des Typs, und sie ist automatisch verfügbar. Package-Objekte halten geteilte Implicits für ein Package. Dieses Design ermöglicht 'Zero-Import'-Type-Class-Verwendung.
// Implicit in companion object is found automatically
case class UserId(value: Long)
object UserId {
implicit val ordering: Ordering[UserId] =
Ordering.by(_.value)
}
// No import needed—companion object implicits are in scope
List(UserId(3), UserId(1), UserId(2)).sorted
// Works because Ordering[UserId] is in UserId's companion
// Implicit scope includes:
// 1. Companion object of the type (UserId)
// 2. Companion object of the type class (Ordering)
// 3. Companion objects of type parameters
// This is why Int has an Ordering:
// object Int { implicit val ord: Ordering[Int] = ... }
// Package object for shared implicits
package object myapp {
implicit val ec: ExecutionContext = ExecutionContext.global
type Id = Long
}Type Classes
Type Class definieren
Eine Type Class ist ein Trait, parametrisiert nach Typ, mit Instanzen, die Verhalten für spezifische Typen bereitstellen. Im Gegensatz zu Vererbung können Sie Type-Class-Instanzen retroaktiv hinzufügen (für Typen, die Sie nicht besitzen). Show[A] definiert, wie A angezeigt wird. Instanzen leben im Companion-Objekt (automatischer impliziter Scope). Dies ist ad-hoc-Polymorphismus – unterschiedliches Verhalten pro Typ ohne Modifikation der Typen. Type Classes sind Scalas mächtigste Abstraktion.
// Type class: a trait parameterized by type
trait Show[A] {
def show(a: A): String
}
// Instances for specific types
object Show {
// Instance for Int
implicit val showInt: Show[Int] = (a: Int) => a.toString
// Instance for String
implicit val showString: Show[String] = (s: String) => s""$s""
// Instance for List (recursive)
implicit def showList[A](implicit s: Show[A]): Show[List[A]] =
(list: List[A]) => list.map(s.show).mkString("[", ", ", "]")
}
// Usage with implicit parameter
def print[A](a: A)(implicit s: Show[A]): Unit =
println(s.show(a))
print(42) // 42
print("hello") // "hello"
print(List(1, 2, 3)) // [1, 2, 3]Type Classes verwenden (Syntax-Zucker)
Context Bounds [A: Show] + summon rufen Type-Class-Instanzen ab. Erweiterungsmethoden (implicit class) fügen Methoden wie .show hinzu, die die Type Class verwenden. Diese Kombination gibt eine saubere API: 42.show funktioniert, wenn Show[Int] existiert. Die Standardbibliothek bietet viele Type Classes: Numeric, Ordering, Eq, Monoid (Cats). Das Importieren von Syntax (Numeric.Implicits._) fügt Operatoren wie + und sum hinzu, die die Type Class verwenden.
// Context bound syntax
def printAll[A: Show](items: List[A]): Unit =
items.foreach(a => println(summon[Show[A]].show(a)))
// Extension methods via implicit class
implicit class ShowOps[A](val a: A) extends AnyVal {
def show(implicit s: Show[A]): String = s.show(a)
}
42.show // "42"
"hi".show // ""hi""
List(1,2).show // "[1, 2]"
// Combining: type class + extension methods
def format[A: Show](a: A): String = a.show
// Standard library type classes
def sum[A: Numeric](xs: List[A]): A =
summon[Numeric[A]].plus(xs.head, xs.tail.foldLeft(
summon[Numeric[A]].zero)(summon[Numeric[A]].plus))
// Or with syntax:
import Numeric.Implicits._
def sum2[A: Numeric](xs: List[A]): A = xs.sumHäufige Type Classes (Cats/Scalaz)
Cats und Scalaz bieten Standard-Type-Classes. Monoid (empty + combine) ermöglicht generische Aggregation. Functor (map) und Monad (pure + flatMap) abstrahieren über Container (List, Option, Future, IO). Eq bietet typsichere Gleichheit (keine versehentlichen Cross-Typ-Vergleiche). Diese komponieren: eine Monad ist ein Functor, ein Monoid ist ein Semigroup. Type Classes ermöglichen das Schreiben von generischem, wiederverwendbarem Code, der über viele Typen hinweg funktioniert.
// Monoid: combine values with empty
trait Monoid[A] {
def empty: A
def combine(a: A, b: A): A
}
object Monoid {
implicit val intAdd: Monoid[Int] = new Monoid[Int] {
def empty = 0
def combine(a: Int, b: Int) = a + b
}
implicit def listMonoid[A]: Monoid[List[A]] = new Monoid[List[A]] {
def empty = Nil
def combine(a: List[A], b: List[A]) = a ++ b
}
}
// Functor: map over structure
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
// Monad: chain operations
trait Monad[F[_]] {
def pure[A](a: A): F[A]
def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
}
// Eq: type-safe equality
trait Eq[A] {
def eqv(a: A, b: A): Boolean
}
// Semigroup: combine (no empty)
trait Semigroup[A] {
def combine(a: A, b: A): A
}Gesetze und Testen von Type Classes
Type-Class-Gesetze sind mathematische Eigenschaften, die Instanzen erfüllen müssen. Monoid erfordert Assoziativität und Identität. Functor erfordert Identität und Kompositions-Erhaltung. Bibliotheken wie Cats bieten Gesetz-Definitionen; discipline + ScalaCheck testet sie automatisch. Gesetze sind der Grund, warum Type Classes mächtig sind: generischer Code (wie foldMap) funktioniert korrekt für jede gesetzeskonforme Instanz. Verifizieren Sie immer, dass Ihre Instanzen die Gesetze erfüllen – Bugs in Instanzen brechen allen generischen Code, der sie verwendet.
// Type class laws: properties that must hold
// Monoid laws:
// 1. Left identity: combine(empty, a) == a
// 2. Right identity: combine(a, empty) == a
// 3. Associativity: combine(a, combine(b, c)) == combine(combine(a, b), c)
// Functor laws:
// 1. Identity: map(fa)(identity) == fa
// 2. Composition: map(fa)(f andThen g) == map(map(fa)(f))(g)
// Testing laws with ScalaCheck (discipline)
import org.scalacheck.Prop.forAll
import cats.kernel.laws.MonoidLaws
class MonoidSpec extends munit.FunSuite with Discipline {
checkAll("Int Monoid", MonoidLaws[Int].monoid)
}
// Custom law test
def monoidLeftIdentity[A](implicit m: Monoid[A], arb: Arbitrary[A]) =
forAll { (a: A) =>
m.combine(m.empty, a) == a
}
// Laws make type classes trustworthy:
// if an instance satisfies laws, generic code works correctlyType-Class-Derivation (Scala 3)
Scala 3 vereinfacht Type-Class-Derivation mit 'derives'-Schlüsselwort und Mirror. Der Compiler kann Instanzen für Case-Klassen und Enums auto-generieren, indem er Element-Instanzen komponiert. Dies eliminiert den Boilerplate, Instanzen für jede Case-Klasse zu schreiben (häufig in Scala 2 mit shapeless). Bibliotheken wie Cats und Circe unterstützen Scala-3-Derivation. Der Mirror-Typ gibt Kompilierzeit-Zugriff auf die Struktur eines Typs (Feldtypen, Labels) für generische Programmierung.
// Scala 3: derive type class instances automatically
import scala.deriving.Mirror
trait Show[A] {
def show(a: A): String
}
object Show {
// Inline given for derivation
given showInt: Show[Int] with
def show(a: Int) = a.toString
given showString: Show[String] with
def show(s: String) = s""$s""
// Derive for products (case classes)
given showProduct[A](using m: Mirror.ProductOf[A])
(using ev: Show[m.MirroredElemTypes]): Show[A] with
def show(a: A): String = ???
// Or use Scala 3's derivation
inline given derive[A](using m: Mirror.Of[A]): Show[A] = ???
}
// Auto-derive for case classes
case class Person(name: String, age: Int) derives Show
// Show[Person] is generated automaticallyFor-Comprehensions Deep Dive
Basis-For-Comprehensions
For-Comprehensions sind syntaktischer Zucker für flatMap/map/withFilter. Jedes <- ist flatMap (außer dem letzten, das map ist). if-Guards werden withFilter. yield macht es eine Collection zurückgeben; das Weglassen von yield macht es imperativ (foreach). Dies funktioniert auf jedem Typ mit flatMap/map (Monad): List, Option, Future, Try, IO. For-Comprehensions zu meistern ist Schlüssel zu idiomatischem Scala – sie ersetzen verschachtelte maps/flatMaps durch lesbare sequenzielle Syntax.
// For comprehension: syntactic sugar for flatMap/map
val result = for {
x <- List(1, 2, 3)
y <- List(10, 20)
} yield x + y
// List(11, 21, 12, 22, 13, 23)
// Desugared:
List(1, 2, 3).flatMap { x =>
List(10, 20).map { y => x + y }
}
// With filters (if guards)
val evens = for {
x <- 1 to 10
if x % 2 == 0
} yield x
// Vector(2, 4, 6, 8, 10)
// Desugared:
(1 to 10).withFilter(_ % 2 == 0).map(identity)
// Without yield: imperative (foreach)
for (x <- 1 to 3) println(x) // 1, 2, 3For mit Option und Future
For-Comprehensions funktionieren auf jeder Monad. Mit Option short-circuiten sie bei None (geben None zurück). Mit Future short-circuiten sie bei Fehlschlag. Dies macht sequenziellen Async/May-Fail-Code wie geradlinigen imperativen Code lesbar, während er funktional bleibt. Jede <- -Zeile kann von vorherigen Bindings abhängen. Dies ist viel sauberer als verschachtelte flatMap-Aufrufe. Dieselbe Syntax funktioniert für Try, Either, IO und benutzerdefinierte Monaden.
// Option: chain operations that might return None
def getUser(id: Int): Option[User] = ???
def getEmail(user: User): Option[String] = ???
val email: Option[String] = for {
user <- getUser(42)
email <- getEmail(user)
} yield email
// Desugared:
getUser(42).flatMap(user => getEmail(user).map(email => email))
// Future: chain async operations
val result: Future[Int] = for {
user <- fetchUser(1) // Future[User]
posts <- fetchPosts(user) // Future[List[Post]]
} yield posts.size
// If any returns None/failed Future, the whole chain short-circuits
// This is the power of monadic compositionFor mit Either und Fehlerbehandlung
Either ist Scalas typisierte Fehlerbehandlung. For-Comprehensions verketten Eithers und short-circuiten bei Left (Fehler). Dies ist funktionale Fehlerbehandlung – keine Exceptions, Fehler sind Werte. Der Left-Typ ist der Fehler (normalerweise String oder ein sealed Trait). Either ist right-biased in Scala 2.12+ (map/flatMap operieren auf Right). Dieses Pattern ersetzt try/catch mit komponierbarer, typsicherer Fehler-Propagation. Cats' Validated ist eine Alternative zum Akkumulieren von Fehlern.
// Either for error handling
def parseAge(s: String): Either[String, Int] =
s.toIntOption.toRight(s"not a number: $s")
def validateAge(age: Int): Either[String, Int] =
if (age >= 0) Right(age) else Left(s"negative: $age")
val result: Either[String, Int] = for {
age <- parseAge("30") // Right(30)
valid <- validateAge(age) // Right(30)
} yield valid
// With error in chain
val error: Either[String, Int] = for {
age <- parseAge("abc") // Left("not a number: abc")
valid <- validateAge(age) // skipped!
} yield valid
// error == Left("not a number: abc")
// Scala 3: for-comprehensions work with Either directly
// (Scala 2 needed either.map(_.right) or withFilter)Desugaring und benutzerdefinierte Monaden
Jeder Typ mit flatMap und map unterstützt For-Comprehensions – dies ist das Monad-Pattern. Definieren Sie diese Methoden auf Ihrem Typ, um For-Syntax zu aktivieren. Das = (nicht <-) erstellt ein lokales Binding (nicht zu flatMap desugared). Desugaring zu verstehen hilft beim Debuggen komplexer Comprehensions und beim Implementieren benutzerdefinierter Monaden. Der Compiler übersetzt For-Comprehensions zu flatMap/map/withFilter-Ketten. Deshalb funktioniert for einheitlich über List, Option, Future, IO usw.
// For-comprehensions require flatMap, map, withFilter
// Define your own monad to use for-comprehensions
case class Box[A](value: A) {
def map[B](f: A => B): Box[B] = Box(f(value))
def flatMap[B](f: A => Box[B]): Box[B] = f(value)
def withFilter(p: A => Boolean): Box[A] =
if (p(value)) this else throw new NoSuchElementException
}
val result = for {
x <- Box(10)
y <- Box(20)
if x < y
} yield x + y
// Box(30)
// Desugaring steps:
// 1. Box(10).flatMap { x =>
// 2. Box(20).withFilter(_ > x... wait, order matters
// Actual:
// Box(10).flatMap(x =>
// Box(20).withFilter(y => x < y).map(y => x + y))
// Assignment within for (= instead of <-)
for {
x <- List(1, 2, 3)
doubled = x * 2 // local val
} yield doubledFor vs. Map/FlatMap (wann verwenden)
Verwenden Sie For-Comprehensions für 2+ abhängige Operationen – sie sind lesbarer als verschachtelte flatMaps. Für eine einzelne Transformation ist map klarer. Zum Abflachen flatMap direkt. Für Nebeneffekte (kein Ergebnis) verwenden Sie for ohne yield. For-Comprehensions glänzen, wenn jeder Schritt vom vorherigen abhängt (monadisches Chaining). Sie machen Async-/Fehlerbehandlungs-Code sequenziell lesbar. Vermeiden Sie tief verschachtelte fors (>5 Level) – extrahieren Sie Helfer für Lesbarkeit.
// For: sequential, dependent operations
val result = for {
user <- fetchUser(id)
profile <- fetchProfile(user.id)
avatar <- fetchAvatar(profile.avatarId)
} yield avatar
// Equivalent with flatMap (harder to read):
fetchUser(id).flatMap(user =>
fetchProfile(user.id).flatMap(profile =>
fetchAvatar(profile.avatarId)))
// Map: single transformation (no chaining)
users.map(_.name) // simple, use map
// FlatMap: when you need to chain but for is overkill
users.flatMap(_.posts) // List[Post]
// For without yield: side effects
for (user <- users) {
saveToDb(user)
sendEmail(user)
}
// Guidelines:
// - 1 operation: use map/flatMap directly
// - 2+ dependent operations: use for-comprehension
// - Side effects: use for without yieldScala 3: Given & Using
Given-Instanzen (ersetzen implicit val)
Scala 3 ersetzt implicit val/def durch 'given'. givens sind klarer und expliziter. Anonyme givens (given Type = ...) haben Compiler-generierte Namen. 'given T with' definiert Instanzen mit mehreren Methoden. Bedingte givens (given [A: Ordering]: Ordering[List[A]]) ersetzen implicit defs. givens in Companion-Objekten sind automatisch im impliziten Scope, wie in Scala 2. Die Schlüsselwort-Änderung reduziert 'implicit'-Überladung (was in Scala 2 4 Dinge bedeutete).
// Scala 2: implicit val
// implicit val ec: ExecutionContext = ExecutionContext.global
// Scala 3: given
given ec: ExecutionContext = ExecutionContext.global
// Anonymous given (inferred name)
given ExecutionContext = ExecutionContext.global
// Given with 'with' for complex types
given Show[Int] with
def show(a: Int): String = a.toString
// Given in companion object (automatic scope)
object MyType:
given Ordering[MyType] = Ordering.by(_.id)
// Conditional given (like implicit def)
given [A: Ordering]: Ordering[List[A]] with
def compare(a: List[A], b: List[A]): Int =
a.zip(b).find((x, y) => x != y) match
case Some((x, y)) => summon[Ordering[A]].compare(x, y)
case None => a.length - b.lengthUsing-Klauseln (ersetzen implizite Parameter)
Scala 3 ersetzt implizite Parameter durch 'using'. Dies trennt die beiden Bedeutungen von 'implicit' (Parameter vs. Konvertierungen). using-Klauseln werden explizit mit 'using'-Schlüsselwort weitergegeben. Context Bounds [A: T] bleiben gleich. summon[T] ersetzt implicitly[T] (klarerer Name). Das using-Schlüsselwort macht offensichtlich, wann Sie einen Kontext bereitstellen. Dies ist eine rein syntaktische Änderung – Semantik sind dieselben wie implizite Parameter.
// Scala 2: implicit parameter
// def process[A](data: List[A])(implicit ec: ExecutionContext): Unit
// Scala 3: using
def process[A](data: List[A])(using ec: ExecutionContext): Unit =
data.foreach(println)
// Context bound (unchanged)
def sort[A: Ordering](list: List[A]): List[A] = list.sorted
// Multiple using clauses
def log(msg: String)(using level: Level, logger: Logger): Unit =
logger.log(level, msg)
// summon replaces implicitly
def max[A: Ordering](a: A, b: A): A =
val ord = summon[Ordering[A]]
if ord.gt(a, b) then a else b
// Provide explicitly with 'using'
process(data)(using myEC)
log("hello")(using Level.INFO, myLogger)Erweiterungsmethoden (Scala 3)
Scala 3 ersetzt implizite Klassen durch 'extension' – klarer und fokussierter. extension (self: T) definiert Methoden auf T. Extensions können generisch sein (extension [A]). Kombiniert mit using bieten sie Type-Class-Syntax (42.show). Extensions sind nur Methoden-Ergänzungen – sie erstellen keine Wrapper-Objekte (Zero-Overhead mit AnyVal in Scala 2, nativ in Scala 3). Dies ist der idiomatische Weg, vorhandenen Typen in Scala 3 Methoden hinzuzufügen.
// Scala 2: implicit class
// implicit class RichInt(val self: Int) extends AnyVal {
// def squared: Int = self * self
// }
// Scala 3: extension
extension (self: Int)
def squared: Int = self * self
def times(f: => Unit): Unit = (1 to self).foreach(_ => f)
5.squared // 25
3.times { println("hi") }
// Extension on generic type
extension [A](self: List[A])
def takeWhileInclusive(p: A => Boolean): List[A] = ???
// Extension with using (type class syntax)
extension [A](self: A) def show(using s: Show[A]): String = s.show(self)
42.show // uses given Show[Int]
// Multiple extensions in one block
extension (s: String)
def isBlank: Boolean = s.trim.isEmpty
def words: List[String] = s.split(" ").toListEnums und ADTs (Scala 3)
Scala-3-Enums ersetzen sealed Trait + Case-Objekte für ADTs. Sie sind prägnanter und unterstützen Parameter, Felder und Methoden. Enum-Cases können Parameter haben (wie Case-Klassen). Pattern-Matching ist erschöpftheit-geprüft. Enums können generisch sein (Option[A]). Dies unifiziert Enums und ADTs in ein Konstrukt. Für offene Hierarchien (erweiterbar) verwenden Sie weiterhin sealed Trait + Case-Klassen. Für geschlossene Enumerationen/ADTs ist enum sauberer.
// Scala 3 enum (replaces sealed trait + case objects)
enum Color:
case Red, Green, Blue
enum HttpStatus(val code: Int):
case Ok extends HttpStatus(200)
case NotFound extends HttpStatus(404)
case Error extends HttpStatus(500)
// Pattern matching (exhaustive)
def describe(c: Color): String = c match
case Color.Red => "red"
case Color.Green => "green"
case Color.Blue => "blue"
// Access fields
HttpStatus.Ok.code // 200
// Parameterized enum cases
enum Option[+A]:
case Some(value: A)
case None
// ADT with methods
enum Tree[+A]:
case Leaf(value: A)
case Node(left: Tree[A], right: Tree[A])
def size: Int = this match
case Leaf(_) => 1
case Node(l, r) => l.size + r.size + 1Top-Level-Definitionen und Einrückung
Scala 3 erlaubt Top-Level-Definitionen – keine Notwendigkeit, alles in ein Object zu wrappen. Dies vereinfacht Datei-Struktur (wie Python/Go). Die neue Syntax unterstützt sowohl Klammern als auch signifikante Einrückung (optional). if/then ersetzt if/else mit Klammern. match kann als Ausdruck ohne Klammern verwendet werden. Diese Änderungen machen Scala 3 zugänglicher, während Rückwärtskompatibilität erhalten bleibt. Sie können Stile mischen – verwenden Sie Klammern, wo sie Klarheit bringen, Einrückung, wo sie Lärm reduzieren.
// Scala 3: top-level definitions (no class wrapper needed)
// File: MyMath.scala
def add(a: Int, b: Int): Int = a + b // top-level function
val Pi: Double = 3.14159 // top-level val
type Id = Long // top-level type alias
given Show[Int] = (a: Int) => a.toString // top-level given
extension (i: Int) def squared: Int = i * i // top-level extension
// Indentation-based syntax (optional, braces still work)
def factorial(n: Int): Int =
if n <= 1 then 1
else n * factorial(n - 1)
// Or with braces:
def factorial2(n: Int): Int = {
if (n <= 1) 1
else n * factorial2(n - 1)
}
// if/then, match/case without braces
val sign = if x > 0 then 1 else -1
val desc = x match
case 0 => "zero"
case _ => "nonzero"Erweiterungsmethoden & Syntax
Implizite Klassen (Scala 2)
implicit class (Scala 2.10+) fügt vorhandenen Typen Erweiterungsmethoden hinzu. Das Erweitern von AnyVal macht es Zero-Allocation (der Compiler löscht den Wrapper). Die Klasse muss einen einzelnen Konstruktor-Parameter nehmen (der erweiterte Typ). Methoden auf der impliziten Klasse werden auf dem erweiterten Typ verfügbar. So reichert Scala Int, String usw. an. Platzieren Sie implizite Klassen in einem Package-Objekt oder Utility-Objekt zum Teilen. In Scala 3 verwenden Sie stattdessen 'extension'.
// Scala 2: implicit class for extension methods
import scala.language.implicitConversions
implicit class RichString(val s: String) extends AnyVal {
def wordCount: Int = s.split("\\s+").length
def slug: String = s.toLowerCase.replaceAll("[^a-z0-9]+", "-")
def encrypt(key: Int): String = s.map(c => (c + key).toChar)
}
"Hello World".wordCount // 2
"My Blog Post!".slug // "my-blog-post-"
"abc".encrypt(1) // "bcd"
// AnyVal avoids allocation (zero overhead)
// implicit class must be in a trait, class, or object
// For generic extensions
implicit class RichList[A](val list: List[A]) extends AnyVal {
def middle: Option[A] = list.lift(list.length / 2)
}
List(1, 2, 3, 4, 5).middle // Some(3)Erweiterungsmethoden in der Praxis
Erweiterungsmethoden sind der Standardweg, Collections und anderen Typen Utilities hinzuzufügen. distinctBy, chunked, tap sind häufige Ergänzungen. Die === und =!= Operatoren (via Eq Type Class) bieten typsichere Gleichheit (im Gegensatz zu ==, das Cross-Typ erlaubt). Gruppieren Sie Erweiterungen in einem Objekt und importieren Sie, wo nötig. Dies hält Core-Typen sauber, während domänenspezifische Methoden erlaubt werden. Scalas Collections-Bibliothek selbst verwendet dieses Pattern extensiv.
// Common pattern: add utility methods to collections
object CollectionExtensions {
implicit class RichSeq[A](val seq: Seq[A]) extends AnyVal {
def chunked(size: Int): Seq[Seq[A]] =
seq.grouped(size).toSeq
def distinctBy[B](f: A => B): Seq[A] =
seq.groupBy(f).values.map(_.head).toSeq
def tap(f: A => Unit): Seq[A] = {
seq.foreach(f)
seq
}
}
}
import CollectionExtensions._
List(1, 2, 3, 4, 5).chunked(2) // List(List(1,2), List(3,4), List(5))
List("aa", "bb", "ab").distinctBy(_.head) // List("aa", "ab")
List(1, 2, 3).tap(println) // prints 1,2,3, returns List(1,2,3)
// Type class syntax via extension
implicit class EqOps[A](val a: A) extends AnyVal {
def ===(b: A)(implicit eq: Eq[A]): Boolean = eq.eqv(a, b)
def =!=(b: A)(implicit eq: Eq[A]): Boolean = !eq.eqv(a, b)
}Implizite Konvertierungen (vorsichtig verwenden)
Implizite Konvertierungen konvertieren automatisch Typen, aber sie sind gefährlich – Code-Verhalten wird nicht offensichtlich. Scala 2.10+ erfordert explizites Opt-in (scala.language.implicitConversions). Bevorzugen Sie Erweiterungsmethoden (die Methoden hinzufügen, ohne Typen zu ändern) über Konvertierungen (die Typen ändern). Legitime Verwendungen: Java-Interop (Konvertieren zwischen Collection-Typen), DSL-Konstruktion. Der Compiler warnt vor impliziten Konvertierungen – nehmen Sie diese Warnungen ernst. In Scala 3 ist given Conversion[T, U] der explizite Mechanismus.
import scala.language.implicitConversions
// Implicit conversion between types
implicit def stringToInt(s: String): Int = s.toInt
val n: Int = "42" // stringToInt("42")
// Dangerous: can cause surprising behavior
implicit def intToBoolean(n: Int): Boolean = n != 0
if (1) println("yes") // works! (intToBoolean(1))
// Safer: use extension methods instead
extension (n: Int) def toBool: Boolean = n != 0
if (1.toBool) println("yes")
// When implicit conversions are appropriate:
// 1. Java interop (java.util.List <-> scala.List)
implicit def javaListToScala[A](jl: java.util.List[A]): List[A] =
import scala.jdk.CollectionConverters._
jl.asScala.toList
// 2. Backward compatibility layers
// 3. DSL construction (use sparingly)
// Enable per-file: import scala.language.implicitConversionsType-Level-Programmierung
Type-Level-Programmierung kodiert Informationen in Typen, die der Compiler prüft. Phantom-Typen (ungenutzte Typ-Parameter) verfolgen Zustand (Open/Closed) und verhindern Missbrauch (kann keine geschlossene Datei lesen). Peano-Naturale repräsentieren Zahlen als Typen. Dies ermöglicht Kompilierzeit-Korrektheitsgarantien – Bugs werden zu Kompilierfehlern. Verwendet in Zustandsautomaten, Maßeinheiten, sized Vectors. Mächtig, aber komplex – verwenden Sie, wenn die Sicherheit die Typ-Komplexität wert ist. Bibliotheken wie shapeless (Scala 2) ermöglichen fortgeschrittene Type-Level-Programmierung.
// Phantom types: track state at type level
sealed trait State
trait Open extends State
trait Closed extends State
class File[S <: State] private (val path: String)
object File {
def open(path: String): File[Open] = new File[Open](path)
}
def close[S <: Open](f: File[S]): File[Closed] =
new File[Closed](f.path) // unsafe cast internally
def read[S <: Open](f: File[S]): String = "data"
// def read[S <: Closed](f: File[S]): String // compile error
val f = File.open("test.txt")
read(f) // OK: f is File[Open]
val closed = close(f)
// read(closed) // COMPILE ERROR: closed is File[Closed]
// Type-level natural numbers (Peano)
sealed trait Nat
trait Zero extends Nat
trait Succ[N <: Nat] extends Nat
// Compile-time list length check
type _0 = Zero
type _1 = Succ[_0]
type _2 = Succ[_1]Opaque Types (Scala 3)
Opaque Types (Scala 3) erstellen Zero-Overhead-Newtypes – im Gegensatz zu Value-Klassen (AnyVal) boxen sie nie und sind zur Laufzeit wirklich nur der zugrunde liegende Typ. Die Typ-Unterscheidung existiert nur zur Kompilierzeit und verhindert Verwechslungen (Celsius vs. Fahrenheit, UserId vs. Long). Innerhalb des definierenden Objekts sind der Typ und sein zugrunde liegender Typ austauschbar; außerhalb sind sie verschieden. Dies ist das 'newtype'-Pattern aus Haskell – domänengetriebene Typen ohne Laufzeitkosten. Verwenden Sie für IDs, Einheiten und Domänen-Primitive.
// Scala 2: value classes for zero-overhead wrappers
// case class UserId(value: Long) extends AnyVal
// Scala 3: opaque types (true zero overhead, no boxing)
object Types:
type UserId = Long
object UserId:
def apply(value: Long): UserId = value
type Email = String
object Email:
def apply(s: String): Email =
require(s.contains("@"), "invalid email")
s
import Types.*
val id: UserId = UserId(42) // just a Long at runtime
val email: Email = Email("[email protected]")
// UserId and Long are NOT interchangeable outside the object
// def wrong(x: Long): UserId = x // ERROR
// But inside the object, they're the same
// Newtype pattern: domain types without overhead
type Celsius = Double
type Fahrenheit = Double
// Prevents mixing up Celsius and Fahrenheit
def toF(c: Celsius): Fahrenheit = c * 9 / 5 + 32Testing (ScalaTest & ScalaCheck)
ScalaTest-Styles
ScalaTest bietet mehrere Styles. FunSuite ist am einfachsten (test("name") { ... }). FlatSpec ist BDD-Style ("A Stack" should "..."). Matchers bietet lesbare Assertionen (shouldBe, should contain, should throw). Wählen Sie einen Style pro Projekt für Konsistenz. FunSuite ist populär für Unit-Tests; FlatSpec für verhaltensfokussierte Tests. Alle Styles unterstützen dieselben Matcher und Lifecycle-Hooks. Der Style beeinflusst Syntax, nicht Fähigkeiten.
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
// FunSuite: simple test functions
class MyTest extends AnyFunSuite with Matchers {
test("addition works") {
1 + 1 should be (2)
1 + 1 shouldBe 2
List(1, 2, 3) should contain (2)
}
test("string operations") {
"hello".length shouldBe 5
"hello" should startWith ("he")
}
}
// FlatSpec: BDD-style
import org.scalatest.flatspec.AnyFlatSpec
class StackSpec extends AnyFlatSpec with Matchers {
"A Stack" should "pop values in LIFO order" in {
val stack = Stack(1, 2, 3)
stack.pop() shouldBe 3
}
it should "throw on empty pop" in {
val stack = Stack()
a [NoSuchElementException] shouldBe thrownBy(stack.pop())
}
}
// WordSpec, FreeSpec, PropSpec also availableAssertionen und Matcher
Matcher bieten ausdrucksstarke Assertionen. shouldBe/should be für Gleichheit. contain, have size, have key für Collections. startWith/endWith/include für Strings. thrownBy für Exceptions. Benutzerdefinierte Matcher (be >, be <=) für Vergleiche. Die 'should'-DSL liest sich wie Englisch und macht Tests selbstdokumentierend. Für komplexe Assertionen verwenden Sie benutzerdefinierte Matcher oder einfaches assert(condition). Vermeiden Sie übermäßiges Verketten von Matchern – Lesbarkeit über Cleverness.
import org.scalatest.matchers.should.Matchers._
// Equality
result shouldBe 42
result should be (42)
result should equal (42)
// Collections
list should contain (5)
list should not contain (0)
list shouldBe empty
list should have size 5
list should contain inOrder (1, 2, 3)
map should contain key ("name")
map should contain value ("Alice")
// Strings
s should startWith ("Hello")
s should endWith ("world")
s should include ("lo")
s should fullyMatch regex "H.*d".r
// Exceptions
a [IOException] should be thrownBy riskyOp()
the [IOException] thrownBy riskyOp() should have message "fail"
// Custom matchers
result should be > 0
result should be <= 100
// Type checks
result shouldBe a [List[_]]
result should be an [IllegalArgumentException]BeforeAndAfter und Fixtures
BeforeAndAfterEach führt Setup/Teardown um jeden Test herum aus. Überschreiben Sie beforeEach/afterEach. Für Ressourcenverwaltung ist das Loan-Pattern (withDb { conn => ... }) sauberer – es stellt Cleanup via try/finally sicher und macht die Ressource explizit. ScalaTest unterstützt auch Fixture-Kontexte (FixtureContext) und geteilte Fixtures via Traits. Bevorzugen Sie das Loan-Pattern oder Fixture-Methoden über veränderlichen beforeEach-Zustand – es ist funktionaler und vermeidet Shared-State-Bugs zwischen Tests.
import org.scalatest.BeforeAndAfterEach
import org.scalatest.funsuite.AnyFunSuite
class DbTest extends AnyFunSuite with BeforeAndAfterEach {
var conn: Connection = _
override def beforeEach(): Unit = {
conn = DriverManager.getConnection("jdbc:h2:mem:test")
conn.execute("CREATE TABLE users (id INT, name VARCHAR)")
}
override def afterEach(): Unit = {
conn.close()
}
test("insert works") {
conn.execute("INSERT INTO users VALUES (1, 'Alice')")
val count = conn.query("SELECT COUNT(*) FROM users")
count shouldBe 1
}
}
// Fixture via loan pattern
class FixtureTest extends AnyFunSuite {
def withDb(test: Connection => Unit): Unit = {
val conn = DriverManager.getConnection("jdbc:h2:mem:test")
try test(conn) finally conn.close()
}
test("query works") {
withDb { conn =>
conn.execute("INSERT ...")
// assertions
}
}
}Property-Based Testing (ScalaCheck)
ScalaCheck generiert zufällige Testeingaben und testet Eigenschaften über viele Fälle. forAll führt die Eigenschaft standardmäßig mit 100 zufälligen Eingaben aus. whenever filtert Eingaben. Benutzerdefinierte Gen-Typen schränken generierte Daten ein (Gen.choose, Gen.nonEmptyListOf). Dies fängt Edge-Cases ab, die Sie mit beispielbasierten Tests verfehlen würden (leere Listen, negative Zahlen, große Werte). Table-driven Tests (Table) sind für spezifische Fälle. Property-Based Testing ist mächtig für reine Funktionen und Datentransformationen – definieren Sie, was immer wahr sein sollte.
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import org.scalacheck.Prop.forAll
class ListSpec extends AnyFunSuite with ScalaCheckPropertyChecks {
// Property: reversing twice = identity
test("reverse twice is identity") {
forAll { (xs: List[Int]) =>
xs.reverse.reverse shouldBe xs
}
}
// Property with conditions
test("head of sorted list is min") {
forAll { (xs: List[Int]) =>
whenever(xs.nonEmpty) {
xs.sorted.head shouldBe xs.min
}
}
}
// Custom generators
import org.scalacheck.Gen
val smallInt = Gen.choose(1, 100)
val nonEmptyList = Gen.nonEmptyListOf(smallInt)
test("custom generator") {
forAll(nonEmptyList) { (xs: List[Int]) =>
xs should not be empty
xs.forall(_ >= 1) shouldBe true
}
}
// Table-driven tests
test("addition table") {
val cases = Table(
("a", "b", "sum"),
(1, 2, 3),
(10, 20, 30),
(-1, 1, 0)
)
forAll(cases) { (a, b, sum) =>
a + b shouldBe sum
}
}
}Mocking und Test Doubles
Mockito (via ScalaTestPlus) erstellt Test Doubles. mock[T] erstellt einen Mock; when(...).thenReturn(...) stubt Methoden; verify prüft Interaktionen. Verwenden Sie Mocks, um die zu testende Einheit von Abhängigkeiten zu isolieren (Datenbanken, APIs). Argument-Matcher (argThat) verifizieren spezifische Argumente. Over-Mocken Sie nicht – wenn Sie alles mocken, testen Sie stattdessen die echte Integration. Bevorzugen Sie Fakes (In-Memory-Implementierungen) über Mocks für komplexe Abhängigkeiten – sie sind robuster und lesbarer.
import org.scalatest.funsuite.AnyFunSuite
import org.scalatestplus.mockito.MockitoSugar
import org.mockito.Mockito._
class UserServiceSpec extends AnyFunSuite with MockitoSugar {
test("getUser returns user from repo") {
// Create mock
val repo = mock[UserRepository]
val user = User(1, "Alice")
// Stub: when X then Y
when(repo.findById(1)).thenReturn(Some(user))
val service = new UserService(repo)
val result = service.getUser(1)
result shouldBe Some(user)
// Verify: was X called?
verify(repo).findById(1)
verify(repo, never()).findById(2)
}
test("createUser saves to repo") {
val repo = mock[UserRepository]
val service = new UserService(repo)
service.createUser("Bob")
// Verify with argument matcher
verify(repo).save(argThat((u: User) => u.name == "Bob"))
}
}
// Stubbing exceptions
when(repo.findById(99)).thenThrow(new RuntimeException("not found"))Higher-Order Functions & FP
Funktionen als Werte
Funktionen sind Werte in Scala – Sie können sie speichern, weitergeben und zurückgeben. (A, B) => C ist der Funktions-Typ. Eta-Expansion (multiply _) konvertiert Methoden zu Funktionswerten. Higher-Order Functions (nehmen/geben Funktionen) ermöglichen mächtige Abstraktionen: map, filter, reduce sind HOFs. Funktionskomposition (andThen, compose) baut Pipelines. Currying (adder(5) gibt eine Funktion zurück) partielle Anwendung. Dies ist die Grundlage der funktionalen Programmierung in Scala.
// Functions are first-class values
val add: (Int, Int) => Int = (a, b) => a + b
val square: Int => Int = x => x * x
// Apply
add(2, 3) // 5
square(4) // 16
// Method to function (eta-expansion)
def multiply(a: Int, b: Int): Int = a * b
val mul = multiply _ // or just 'multiply' in Scala 3
mul(3, 4) // 12
// Higher-order: function taking function
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(square, 2) // 16
// Higher-order: function returning function
def adder(n: Int): Int => Int = _ + n
val add5 = adder(5)
add5(10) // 15
// Function composition
val f: Int => Int = _ + 1
val g: Int => Int = _ * 2
val h = f andThen g // f then g: (x+1)*2
val h2 = f compose g // g then f: (x*2)+1
h(3) // 8
h2(3) // 7Currying und partielle Anwendung
Currying teilt eine Funktion in mehrere Parameterlisten und ermöglicht partielle Anwendung (einige Argumente fixieren, eine Funktion für den Rest erhalten). Dies hilft Typ-Inferenz (frühere Parameter schränken spätere ein) und erstellt konfigurierbare Funktionen (withDb(config)). Der Platzhalter _ wendet partiell an: sum(1, _) erstellt eine Funktion. Mehrere Parameterlisten sind Scalas Currying-Mechanismus. Verwenden Sie Currying, wenn einige Argumente 'Konfiguration' und andere 'Daten' sind.
// Curried function: multiple parameter lists
def add(a: Int)(b: Int): Int = a + b
val add5: Int => Int = add(5) // partial application
add5(10) // 15
// Multiple parameter lists for type inference
def map[A, B](list: List[A])(f: A => B): List[B] =
list.map(f)
map(List(1, 2, 3))(x => x * 2) // A, B inferred from first list
// Curried form (Function types)
val curriedAdd: Int => Int => Int = a => b => a + b
curriedAdd(5)(10) // 15
// Uncurrying
def uncurriedAdd(a: Int, b: Int): Int = a + b
// Practical: configuration via currying
def withDb(config: DbConfig)(f: Connection => Result): Result = ???
val withMyDb = withDb(myConfig) _
withMyDb { conn => /* ... */ }
// Partial application with placeholder
val sum: (Int, Int) => Int = _ + _
val addOne: Int => Int = sum(1, _)Reine Funktionen und Referenzielle Transparenz
Reine Funktionen geben immer dieselbe Ausgabe für dieselbe Eingabe zurück und haben keine Nebeneffekte. Sie sind einfach zu testen, zu begründen, zu parallelisieren und zu komponieren. Referenzielle Transparenz bedeutet, dass Sie einen Funktionsaufruf durch sein Ergebnis ersetzen können, ohne das Verhalten zu ändern. Scala erzwingt keine Reinheit, aber Bibliotheken wie Cats Effect (IO-Monad) lassen Sie Nebeneffekte isolieren. Reiner Core + IO an den Rändern ist eine häufige FP-Architektur: Geschäftslogik ist rein, I/O ist in IO gewrappt.
// Pure function: same input → same output, no side effects
def pureAdd(a: Int, b: Int): Int = a + b // pure
// Impure: depends on external state
var counter = 0
def impureAdd(a: Int): Int = { counter += 1; a + counter } // impure
// Impure: side effect
def impurePrint(a: Int): Int = { println(a); a } // side effect
// Referential transparency: can replace call with result
val x = pureAdd(2, 3)
val y = x + x // same as pureAdd(2, 3) + pureAdd(2, 3)
// Benefits of purity:
// 1. Easy to test (no setup/teardown)
// 2. Easy to reason about (no hidden state)
// 3. Parallelizable (no shared state)
// 4. Memoizable (cache results)
// 5. Composable (predictable)
// IO monad for side effects (Cats Effect)
import cats.effect.IO
val program: IO[Unit] = IO.println("hello")
val mapped: IO[String] = IO.pure("world").map(_.toUpperCase)
// Side effects captured in IO, run at edge of programUnveränderliche Daten und persistente Collections
Unveränderliche Datenstrukturen geben bei Modifikation neue Kopien zurück und teilen intern Struktur für Effizienz (persistente Datenstrukturen). Scalas Collections sind standardmäßig unveränderlich. case class copy erstellt modifizierte Kopien. Für tiefe Updates bieten Lens-Bibliotheken (Monocle) komponierbare Accessoren. Unveränderlichkeit eliminiert ganze Klassen von Bugs (Race-Conditions, unerwartete Mutationen) und macht Code leichter zu begründen. Die Performance-Kosten sind oft akzeptabel aufgrund von strukturellem Teilen (O(log n) nicht O(n)).
// Immutable: operations return new collections
val list1 = List(1, 2, 3)
val list2 = list1 :+ 4 // List(1, 2, 3, 4)
// list1 is unchanged: List(1, 2, 3)
// Persistent data structures: share structure (efficient)
val map1 = Map("a" -> 1, "b" -> 2)
val map2 = map1 + ("c" -> 3) // shares structure with map1
// O(log n) due to structural sharing, not O(n) copy
// Case classes: copy for modification
case class User(name: String, age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31) // new instance, alice unchanged
// Lens (Monocle library) for nested updates
import monocle.macros.GenLens
val ageLens = GenLens[User](_.age)
val updated = ageLens.modify(_ + 1)(alice) // User("Alice", 31)
// Benefits:
// - No bugs from shared mutation
// - Easy to reason about (values don't change)
// - Free concurrency (no locks needed)
// - Undo/redo trivial (keep old versions)Rekursion und Tail Calls
Tail-Rekursion (wo der rekursive Aufruf die letzte Operation ist) wird vom Scala-Compiler zu einer Schleife optimiert – kein Stack-Wachstum. @tailrec verifiziert dies zur Kompilierzeit. Das Akkumulator-Pattern (akkumuliertes Ergebnis weitergeben) konvertiert Nicht-Tail-Rekursion zu Tail-Rekursion. foldLeft/foldRight kapseln häufige Rekursions-Pattern. Für tiefe Nicht-Tail-Rekursion verwenden Sie Trampoline (Cats) oder schreiben mit Folds um. Bevorzugen Sie Folds über explizite Rekursion für Klarheit – sie sind tail-rekursiv und idiomatisch.
// Regular recursion (can stack overflow)
def factorial(n: Int): Int =
if (n <= 1) 1 else n * factorial(n - 1)
factorial(10000) // StackOverflowError!
// Tail recursion: compiler optimizes to loop
import scala.annotation.tailrec
@tailrec
def factorialTail(n: Int, acc: Int = 1): Int =
if (n <= 1) acc else factorialTail(n - 1, n * acc)
factorialTail(10000) // works (no stack overflow)
// @tailrec annotation: compiler verifies it's tail-recursive
// Error if not actually tail-recursive
// Trampoline for non-tail recursion
// (Cats' Trampoline or Free monads)
// Converts stack recursion to heap
// Fold as alternative to recursion
def sum(list: List[Int]): Int = list.foldLeft(0)(_ + _)
// foldLeft is tail-recursive internally
// Pattern: accumulator pattern
@tailrec
def reverse[A](list: List[A], acc: List[A] = Nil): List[A] =
list match
case Nil => acc
case head :: tail => reverse(tail, head :: acc)Case-Klassen & ADTs
Case-Klasse-Grundlagen
Case-Klassen sind Scalas primäres Datenmodellierungs-Tool. Der Compiler generiert equals, hashCode, toString, copy, apply, unapply (für Pattern-Matching) und Accessor-Methoden. Sie sind standardmäßig unveränderlich (val-Felder). Verwenden Sie Case-Klassen für DTOs, Value Objects, Messages und Domänen-Entitäten (wenn unveränderlich). Das Companion-Objekts apply lässt Sie ohne 'new' konstruieren. copy ermöglicht nicht-zerstörende Updates. Case-Klassen sind die Grundlage von ADTs (Algebraic Data Types).
// Case class: immutable data holder with boilerplate generated
case class Person(name: String, age: Int)
val alice = Person("Alice", 30)
val bob = Person("Bob", 25)
// Auto-generated methods:
alice.toString // "Person(Alice,30)"
alice == Person("Alice", 30) // true (structural equality)
alice.hashCode // based on fields
// Pattern matching
alice match
case Person(name, age) => s"$name is $age"
// Copy with modifications
val older = alice.copy(age = 31) // Person("Alice", 31)
// Companion object with apply (no 'new' needed)
val p = Person("Charlie", 40) // Person.apply called
// Fields accessed directly
alice.name // "Alice"
alice.age // 30
// Case class with default values
case class Point(x: Double = 0, y: Double = 0)
Point() // Point(0.0, 0.0)
Point(y = 5) // Point(0.0, 5.0)Sealed Traits und ADTs
ADTs (Algebraic Data Types) modellieren Daten als geschlossene Menge von Fällen (sealed Trait + Case-Klassen/-Objekte). 'sealed' bedeutet, dass alle Subtypen in derselben Datei sind, was erschöpfendes Pattern-Matching ermöglicht – der Compiler warnt, wenn Sie einen Fall verpassen. Das Hinzufügen eines neuen Falls zeigt alle Stellen, die Updates benötigen (sicheres Refactoring). Case-Objekte sind Singletons (keine Parameter). Option, List, Either sind alle ADTs. Dies ist der funktionale Weg, Domänen zu modellieren – jede Variante ist ein Fall, Verhalten ist im Pattern-Matching.
// ADT: sealed trait + case classes/objects
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(a: Double, b: Double, c: Double) extends Shape
// Exhaustive pattern matching (compiler checks all cases)
def area(shape: Shape): Double = shape match
case Circle(r) => math.Pi * r * r
case Rectangle(w, h) => w * h
case Triangle(a, b, c) =>
val s = (a + b + c) / 2
math.sqrt(s * (s-a) * (s-b) * (s-c))
// Adding a new case: compiler warns about non-exhaustive matches
case class Square(side: Double) extends Shape
// area now needs a Square case!
// Case objects for singletons (no parameters)
sealed trait Status
case object Active extends Status
case object Inactive extends Status
case object Pending extends Status
// Option and List are ADTs:
// sealed trait Option[+A]
// case class Some[A](value: A) extends Option[A]
// case object None extends Option[Nothing]Case-Klassen-Features
Case-Klassen können Methoden, benutzerdefiniertes apply (für Validierung), Traits implementieren und generisch sein. Benutzerdefiniertes apply im Companion-Objekt kann vor Konstruktion validieren (illegale Zustände unrepräsentierbar machen). Case-Klassen können Traits für Polymorphismus implementieren. Der private Konstruktor (case class Email private) erzwingt die Verwendung des Companion-apply für Validierung. Dies kombiniert Datenmodellierung mit Kapselung – Konstruktoren, die nicht stillschweigend fehlschlagen können.
// Case class with methods
case class Vec2(x: Double, y: Double) {
def +(other: Vec2): Vec2 = Vec2(x + other.x, y + other.y)
def magnitude: Double = math.sqrt(x*x + y*y)
def dot(other: Vec2): Double = x*other.x + y*other.y
}
Vec2(1, 2) + Vec2(3, 4) // Vec2(4.0, 6.0)
Vec2(3, 4).magnitude // 5.0
// Case class with custom apply (validation)
case class Email private (value: String)
object Email {
def apply(value: String): Email = {
require(value.contains("@"), "invalid email")
new Email(value) // bypass public apply
}
}
// Email("notanemail") // IllegalArgumentException
Email("[email protected]") // works
// Case class implementing trait
trait Jsonable {
def toJson: String
}
case class User(name: String, age: Int) extends Jsonable {
def toJson: String = s"""{"name":"$name","age":$age}"""
}
// Case class with type parameters
case class Box[A](value: A) {
def map[B](f: A => B): Box[B] = Box(f(value))
}Pattern-Matching Deep Dive
Pattern-Matching ist Scalas mächtiges Destrukturierungs-Tool. Es unterstützt: Literal-Patterns (0, "hello"), Typ-Patterns (s: String), Case-Klassen-Patterns (User(name, age)), Guards (if condition), verschachtelte Patterns und Binding (@). Der Compiler prüft Erschöpftheit für Sealed-Typen. Patterns werden top-to-bottom versucht. Verwenden Sie @, um den ganzen Wert zu binden, während Teile extrahiert werden. Pattern-Matching ersetzt if-else-Ketten, Typ-Prüfungen und Destrukturierung mit einer einheitlichen, lesbaren Syntax.
// Various pattern types
val x: Any = (1, "hello")
x match
case (a: Int, b: String) => s"int $a, string $b"
case (a, b) => s"pair: $a, $b"
case _ => "other"
// Guards
def classify(n: Int): String = n match
case n if n < 0 => "negative"
case 0 => "zero"
case n if n % 2 == 0 => "even positive"
case _ => "odd positive"
// Case class patterns (nested)
case class Point(x: Int, y: Int)
case class Shape(center: Point, size: Int)
def isOrigin(s: Shape): Boolean = s match
case Shape(Point(0, 0), _) => true
case _ => false
// Named patterns (bind the whole while extracting)
case class User(name: String, age: Int)
def describe(u: User): String = u match
case u @ User(name, age) if age < 18 => s"$name is a minor"
case User(name, _) => s"$name is an adult"
// Type patterns
def handle(x: Any): String = x match
case s: String => s"string: $s"
case n: Int => s"int: $n"
case list: List[_] => s"list of size ${list.size}"
case None => "none"
case _ => "unknown"Extraktoren und benutzerdefinierte Patterns
Extraktoren (unapply) ermöglichen Pattern-Matching auf jedem Typ, nicht nur Case-Klassen. Dies entkoppelt das Matching-Pattern von der Datenrepräsentation. Regexes sind Extraktoren (Gruppen werden Bindings). Sie können Extraktoren für externe Typen (JSON, URLs) schreiben, ohne sie zu modifizieren. unapply gibt Option[(T1, T2, ...)] für Extraktion oder Boolean für einfaches Matching zurück. Extraktoren machen Pattern-Matching auf jede Datenquelle erweiterbar. Case-Klassen auto-generieren unapply; benutzerdefinierte Extraktoren fügen Matching für Nicht-Case-Klassen-Typen hinzu.
// Custom extractor via unapply
object Email {
def unapply(str: String): Option[(String, String)] = {
val parts = str.split("@")
if (parts.length == 2) Some((parts(0), parts(1)))
else None
}
}
// Use in pattern matching
"[email protected]" match
case Email(user, domain) => s"user: $user, domain: $domain"
case _ => "not an email"
// Boolean extractor (no extracted values)
object Even {
def unapply(n: Int): Boolean = n % 2 == 0
}
5 match
case Even() => "even"
case _ => "odd"
// Extractor with variable arity
object Pair {
def unapply[A, B](t: (A, B)): Option[(A, B)] = Some(t._1, t._2)
}
// Regex as extractor
val Date = "(\\d{4})-(\\d{2})-(\\d{2})".r
"2024-01-15" match
case Date(year, month, day) => s"$year/$month/$day"
case _ => "not a date"
// Practical: parse without case classes
object Json {
def unapply(s: String): Option[Any] =
scala.util.Try(ujson.read(s)).toOption
}Collections Deep
Unveränderliche Collections
Scala-Collections sind standardmäßig unveränderlich. List ist eine Linked List (O(n) Random-Access). Vector ist ein Baum mit O(log n) Zugriff. Map und Set sind hash-basiert. Operationen geben neue Collections zurück und teilen Struktur für Effizienz.
val list = List(1, 2, 3)
val vector = Vector(1, 2, 3) // Fast random access
val set = Set(1, 2, 3)
val map = Map("a" -> 1, "b" -> 2)
// All immutable: operations return new collections
val updated = map + ("c" -> 3) // New mapCollection-Operationen
map transformiert Elemente. filter wählt aus. reduce kombiniert. grouped chunkt. flatten merged verschachtelte Collections. flatMap mappt und flacht ab. Alle geben neue Collections zurück. Verzögerte Collections verwenden .view oder .iterator für Effizienz.
val nums = (1 to 10).toList
val doubled = nums.map(_ * 2)
val evens = nums.filter(_ % 2 == 0)
val sum = nums.reduce(_ + _)
val grouped = nums.grouped(3).toList // List(List(1,2,3), List(4,5,6), ...)
val flat = List(List(1,2), List(3,4)).flatten // List(1,2,3,4)Pattern-Matching Collections
Pattern-Matching funktioniert auf Collections. :: (cons) matcht Head und Tail. List(a, b, c) matcht genau 3 Elemente. Nil matcht leere Liste. _ ist Wildcard. Nützlich für Parsing und Destrukturierung. Erschöpfendes Matching verhindert Bugs.
val list = List(1, 2, 3)
list match {
case head :: tail => println(s"Head: $head")
case Nil => println("Empty")
}
List(1, 2, 3) match {
case List(a, b, c) => println(s"$a, $b, $c")
case _ => println("Other")
}Veränderliche Collections
Veränderliche Collections (ArrayBuffer, mutable.Set, mutable.Map) modifizieren In-Place. Schneller für häufige Updates, aber nicht Thread-sicher. Verwenden Sie, wenn Performance wichtig und Unveränderlichkeit nicht nötig. Konvertieren Sie zu unveränderlich mit .toMap, .toSet.
import scala.collection.mutable
val buffer = mutable.ArrayBuffer(1, 2, 3)
buffer += 4 // Add element
buffer -= 2 // Remove element
buffer(0) = 10 // Update
val mset = mutable.Set(1, 2, 3)
val mmap = mutable.Map("a" -> 1)
mmap("b") = 2Verzögerte Collections
view erstellt eine verzögerte View: Operationen werden bis zum Forcen aufgeschoben. Nützlich für verkettete Operationen auf großen Collections. LazyList (Scala 2.13+) ist eine verzögerte Sequenz. Unendliche Sequenzen sind mit verzögerter Auswertung möglich. Forcen mit .toList, .toArray.
val lazyView = (1 to 1000000).view.map(_ * 2).filter(_ > 100)
// No computation yet
val first = lazyView.head // Only computes first
val result = lazyView.take(10).toList // Only 10 elements processed
// LazyList (formerly Stream)
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)Implicits
Implizite Parameter
Implizite Parameter werden automatisch weitergegeben, wenn im Scope. Der Compiler sucht nach einem passenden impliziten Wert. Verwendet für Konfiguration, Type Classes und Kontext. Kann explizit überschrieben werden. Mehrere Implicits müssen unterschiedliche Typen haben.
def connect(url: String)(implicit timeout: Int): Unit = {
println(s"Connecting to $url with timeout $timeout")
}
implicit val defaultTimeout: Int = 5000
connect("http://example.com") // Uses 5000
connect("http://example.com")(3000) // Explicit overrideImplizite Konvertierungen
Implizite Konvertierungen konvertieren automatisch Typen. Können gefährlich sein (unerwartete Konvertierungen). implicit class fügt Erweiterungsmethoden hinzu. Scala 3 verwendet given/using und extension für Klarheit. Bevorzugen Sie Erweiterungsmethoden über rohe Konvertierungen.
import scala.language.implicitConversions
implicit def intToString(n: Int): String = n.toString
val s: String = 42 // Converts via intToString
// Extension methods (Scala 3 preferred)
implicit class RichInt(val n: Int) extends AnyVal {
def squared: Int = n * n
}
5.squared // 25Type Classes
Type Classes sind ein Pattern für ad-hoc-Polymorphismus. Ein Trait definiert Verhalten, Instanzen bieten Implementierungen für spezifische Typen. Implizite Auflösung findet die Instanz. Flexibler als Vererbung. Häufig in Cats und Shapeless.
trait Show[A] {
def show(a: A): String
}
object Show {
implicit val intShow: Show[Int] = (a: Int) => a.toString
implicit val stringShow: Show[String] = (a: String) => a
def apply[A](a: A)(implicit s: Show[A]): String = s.show(a)
}
Show(42) // "42"
Show("hello") // "hello"Context Bounds
Context Bounds [A: TypeClass] sind syntaktischer Zucker für implizite Parameter. Die Type-Class-Instanz ist via implicitly verfügbar. Sauberere Syntax für Type-Class-Constraints. Häufig mit Ordering, Numeric und benutzerdefinierten Type Classes.
def max[A: Ordering](a: A, b: A): A = {
val ord = implicitly[Ordering[A]]
if (ord.gt(a, b)) a else b
}
// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
if (ord.gt(a, b)) a else bGiven/Using (Scala 3)
Scala 3 ersetzt Implicits durch given/using für Klarheit. given definiert eine Instanz, using deklariert einen Parameter. extension ersetzt implicit class. Expliziter und lesbarer. Migrations-Tools konvertieren Scala-2-Implicits.
// Scala 3 syntax
given defaultTimeout: Int = 5000
def connect(url: String)(using timeout: Int): Unit =
println(s"Connecting with $timeout")
connect("http://example.com") // Uses given
// Extension methods
extension (n: Int)
def squared: Int = n * nNebenläufigkeit
Future
Future repräsentiert eine asynchrone Berechnung. onComplete behandelt Vervollständigung. map/flatMap verketten Operationen. ExecutionContext stellt Threads bereit. Futures sind unveränderlich und einmalig. Verwenden Sie For-Comprehension für mehrere Futures.
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f: Future[Int] = Future {
Thread.sleep(1000)
42
}
f.onComplete {
case Success(v) => println(v)
case Failure(e) => println(e)
}
// Map/flatMap for chaining
val f2 = f.map(_ * 2).flatMap(x => Future(x + 1))For-Comprehension mit Futures
For-Comprehension desugart zu flatMap/map. Sequenziell: jeder Schritt wartet auf den vorherigen. Für parallele Ausführung starten Sie Futures vor der for. Viel lesbarer als verschachtelte Callbacks. Funktioniert mit jeder Monad (Future, Option, List).
def getUser(id: Int): Future[User] = ...
def getOrders(user: User): Future[List[Order]] = ...
val result: Future[List[Order]] = for {
user <- getUser(1)
orders <- getOrders(user)
} yield orders
// Equivalent to flatMap/map chainingParallele Futures
Starten von Futures vor der For-Comprehension führt sie parallel aus. Future.sequence konvertiert List[Future[T]] zu Future[List[T]]. Future.traverse mappt und sequenziert in einem Schritt. zip kombiniert zwei Futures. Alle vervollständigen, wenn der langsamste fertig ist.
val f1 = Future { compute1() }
val f2 = Future { compute2() }
val combined: Future[(Int, Int)] = for {
r1 <- f1
r2 <- f2
} yield (r1, r2)
// Or: Future.sequence(List(f1, f2))
// Or: Future.traverse(list)(compute)Actors (Akka)
Akka-Actors kapseln Zustand und kommunizieren via Messages. Kein geteilter veränderlicher Zustand. Jeder Actor verarbeitet eine Message gleichzeitig. ! (tell) sendet Fire-and-Forget. ? (ask) gibt ein Future zurück. Supervision behandelt Fehler. Ideal für nebenläufige zustandsbehaftete Systeme.
import akka.actor.*
class Counter extends Actor {
var count = 0
def receive = {
case "inc" => count += 1
case "get" => sender() ! count
}
}
val system = ActorSystem("mySystem")
val counter = system.actorOf(Props[Counter], "counter")
counter ! "inc"
counter ! "get"Cats Effect IO
Cats Effect IO ist eine reine funktionale IO-Monad. Referenziell transparent: IO(println("x")) ist ein Wert. Komponiert mit For-Comprehension. Abbrechbar und ressourcensicher. unsafeRunSync läuft am Rand Ihres Programms. Alternative zu Future mit besseren Semantiken.
import cats.effect.IO
val program: IO[Int] = for {
_ <- IO(println("Start"))
result <- IO.pure(42)
_ <- IO(println(s"Got $result"))
} yield result
// Run at end of world
program.unsafeRunSync()
// Referentially transparent, cancellablePattern-Matching Deep
Case-Klassen
Case-Klassen auto-generieren equals, hashCode, toString und apply/unapply. Perfekt für Pattern-Matching. Pattern-Matching destrukturiert sie. Standardmäßig unveränderlich. Verwenden Sie für Algebraic Data Types. copy() erstellt modifizierte Kopien.
case class Point(x: Int, y: Int)
val p = Point(1, 2)
p match {
case Point(0, 0) => "origin"
case Point(0, _) => "on y-axis"
case Point(x, 0) => s"on x-axis at $x"
case Point(x, y) => s"at ($x, $y)"
}Sealed Traits
Sealed Traits beschränken Subtypen auf dieselbe Datei. Der Compiler prüft Erschöpftheit im Pattern-Matching. Das Hinzufügen eines neuen Subtyps verursacht Warnungen in allen Matches. Ideal für geschlossene Typ-Hierarchien. Kombiniert mit Case-Klassen für ADTs.
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Square(side: Double) extends Shape
case class Rectangle(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
case Circle(r) => math.Pi * r * r
case Square(s) => s * s
case Rectangle(w, h) => w * h
} // Compiler warns if not exhaustiveGuards & Extraktoren
Guards (if) fügen Patterns Bedingungen hinzu. Benutzerdefinierte Extraktoren (unapply) ermöglichen Pattern-Matching auf jedem Typ. unapply gibt Option zurück, um Match anzuzeigen. Extraktoren entkoppeln Matching vom Typ. Mächtig für DSLs und Parsing.
def classify(n: Int): String = n match {
case x if x < 0 => "negative"
case 0 => "zero"
case x if x % 2 == 0 => "even"
case _ => "odd"
}
// Custom extractor
object Even {
def unapply(n: Int): Option[Int] =
if (n % 2 == 0) Some(n / 2) else None
}
4 match { case Even(half) => s"half is $half" }Partielle Funktionen
Partielle Funktionen sind nur für einige Eingaben definiert. isDefinedAt prüft. collect wendet nur an, wo definiert. Nützlich für Callbacks und Routing. orElse kombiniert partielle Funktionen. lift konvertiert zu totaler Funktion, die Option zurückgibt.
val pf: PartialFunction[Int, String] = {
case 1 => "one"
case 2 => "two"
}
pf.isDefinedAt(1) // true
pf.isDefinedAt(3) // false
// Collect = filter + map
List(1, 2, 3, 1).collect(pf) // List("one", "two", "one")Pattern-Matching-Typen
Pattern-Matching funktioniert auf Typen, aber Type-Erasure beeinflusst Generics. List(a, b) matcht eine 2-elementige Liste. List(_*) matcht jede Liste. Vermeiden Sie Matching auf generischen Typen wie List[Int] (erased). Verwenden Sie Type-Tags für Laufzeit-Typ-Info.
def describe(x: Any): String = x match {
case i: Int => s"Int: $i"
case s: String => s"String: $s"
case List(a, b) => s"Two-element list: $a, $b"
case List(_*) => "List with elements"
case Some(v) => s"Some: $v"
case None => "None"
case _ => "Unknown"
}Funktionale Programmierung
Higher-Order Functions
Higher-Order Functions nehmen oder geben Funktionen. Currying teilt Multi-Arg-Funktionen in Single-Arg-Ketten. Partielle Anwendung fixiert einige Argumente. Ermöglicht Funktionskomposition und Wiederverwendung. _ erstellt eine partiell angewandte Funktion.
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(_ + 3, 5) // 11
// Currying
def add(a: Int)(b: Int): Int = a + b
val add5 = add(5) _ // Partial application
add5(3) // 8Option & Either
Option repräsentiert optionale Werte: Some oder None. Vermeidet null. getOrElse bietet Standard. Either repräsentiert Erfolg (Right) oder Fehlschlag (Left). Besser als Exceptions für erwartete Fehler. Beide sind Monaden: map, flatMap, For-Comprehension.
def find(id: Int): Option[String] =
if (id > 0) Some("Alice") else None
find(1).getOrElse("Unknown") // "Alice"
find(-1).getOrElse("Unknown") // "Unknown"
// Either for error handling
def parse(s: String): Either[String, Int] =
try Right(s.toInt)
catch { case _: Exception => Left(s"Not a number: $s") }Funktionskomposition
compose verkettet Funktionen rechts nach links (wie Mathematik). andThen verkettet links nach rechts (lesbarer). Beide erstellen neue Funktionen. Nützlich zum Bauen von Pipelines. Funktionen sind First-Class-Werte in Scala.
val addOne: Int => Int = _ + 1
val double: Int => Int = _ * 2
// Compose (right to left)
val f = addOne compose double // double then addOne
f(3) // 7
// AndThen (left to right)
val g = addOne andThen double // addOne then double
g(3) // 8Rekursion & Tail-Rekursion
Tail-Rekursion wird vom Compiler zu einer Schleife optimiert. Der rekursive Aufruf muss die letzte Operation sein. @tailrec-Annotation verifiziert dies zur Kompilierzeit. Akkumulator-Pattern trägt Zustand. Verhindert Stack-Overflow bei tiefer Rekursion.
// Not tail-recursive: stack overflow for large n
def factorial(n: Int): Int =
if (n <= 1) 1 else n * factorial(n - 1)
// Tail-recursive: optimized to loop
import scala.annotation.tailrec
@tailrec
def factorial(n: Int, acc: Int = 1): Int =
if (n <= 1) acc else factorial(n - 1, n * acc)Monaden
Monaden haben flatMap und unit (pure/pure). Sie verketten Operationen mit Kontext (Option: Abwesenheit, List: Nicht-Determinismus, Future: Async). For-Comprehension desugart zu flatMap/map. Monad-Gesetze stellen korrekte Komposition sicher. Cats bietet Monad-Type-Class.
// Monad laws: left identity, right identity, associativity
// Option is a monad:
Some(5).flatMap(x => Some(x + 1)) // Some(6)
None.flatMap(x => Some(x + 1)) // None
// List is a monad:
List(1, 2).flatMap(x => List(x, x * 10)) // List(1, 10, 2, 20)
// for-comprehension is monadic sugar:
for {
x <- Some(5)
y <- Some(x + 1)
} yield y // Some(6)Häufige Fallstricke
Null vs. Option
Scala hat null für Java-Kompatibilität, aber es ist verpönt. Option repräsentiert Abwesenheit explizit. Pattern-Matching erzwingt das Behandeln von None. Verwenden Sie .toOption auf Try für Exceptions. Vermeiden Sie null in Scala-Code; reservieren Sie für Java-Interop.
// BAD: null
def find(id: Int): String =
if (id > 0) "Alice" else null
// GOOD: Option
def find(id: Int): Option[String] =
if (id > 0) Some("Alice") else None
// Scala avoids null; use Option
// NullPointer exceptions are rare in idiomatic ScalaVar vs. Val
val ist unveränderlich (Wert), var ist veränderlich (Variable). Bevorzugen Sie val für sichereren, vorhersagbareren Code. Veränderlicher Zustand kompliziert Begründung und Nebenläufigkeit. Verwenden Sie var nur für lokale Performance oder, wenn wirklich nötig. Collections sind standardmäßig unveränderlich.
var x = 1 // Mutable
x = 2 // OK
val y = 1 // Immutable
// y = 2 // Error
// Prefer val for immutability
// Use var only when necessary
// Mutable state causes bugs in concurrent codeGleichheit
Scala == ruft equals auf (Wertgleichheit), im Gegensatz zu Java. eq prüft Referenzgleichheit. ne ist die Negation von eq. Verwenden Sie immer == für Wertvergleich. Case-Klassen haben korrektes equals. Für benutzerdefinierte Klassen überschreiben Sie equals und hashCode.
val a = List(1, 2)
val b = List(1, 2)
a == b // true (value equality)
a eq b // false (reference equality)
// In Java: a == b compares references
// In Scala: == calls equals (value)
// Use eq for reference equality (rarely needed)Implizite Mehrdeutigkeit
Mehrere Implicits desselben Typs verursachen Mehrdeutigkeitsfehler. Halten Sie impliziten Scope sauber. Verwenden Sie spezifische Typen (Newtypes), um zu unterscheiden. Scala-3 given/using ist klarer. Vermeiden Sie implizite Konvertierungen; sie verursachen überraschendes Verhalten.
implicit val s1: String = "hello"
implicit val s2: String = "world"
def greet(implicit s: String) = println(s)
greet // Error: ambiguous implicit values
// Fix: only one implicit in scope
// Or: pass explicitly
greet(s1)By-Name vs. By-Value
By-Name-Parameter (=> T) werden verzögert ausgewertet, bei jeder Verwendung. By-Value-Parameter werden einmal vor dem Aufruf ausgewertet. By-Name ermöglicht benutzerdefinierte Kontrollstrukturen (unless, while). Kann mehrfache Auswertung verursachen; cachen Sie mit lazy val, falls nötig.
// By-value: evaluated once
def log(msg: String) = println(msg)
log(expensiveComputation()) // Evaluated before call
// By-name: evaluated each use
def log(msg: => String) = println(msg)
log(expensiveComputation()) // Evaluated only if called
// Useful for lazy evaluation and control flow
def unless(cond: Boolean)(body: => Unit): Unit =
if (!cond) bodyAkka Actors
Actor-System
ActorSystem ist der Container für Actors. actorOf erstellt Actors mit einem Namen. ! (tell) sendet eine Message asynchron. ? (ask) sendet und gibt ein Future zurück. Actors werden nach Pfad identifiziert. terminate fährt das System kontrolliert herunter.
import akka.actor.*
val system = ActorSystem("MySystem")
val actor = system.actorOf(Props[MyActor], "myActor")
actor ! "Hello" // Fire-and-forget
val future = actor ? "Query" // Ask pattern
system.terminate()Actor-Lifecycle
preStart läuft bei Erstellung, postStop bei Termination. preRestart/postRestart behandeln Supervision. Der Actor wird vor Restart gestoppt, dann frisch gestartet. Interner Zustand geht bei Restart verloren. Verwenden Sie preStart für Initialisierung und postStop für Cleanup.
class MyActor extends Actor {
override def preStart(): Unit = println("Starting")
override def postStop(): Unit = println("Stopped")
override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
println("Restarting")
super.preRestart(reason, message)
}
def receive = {
case "ping" => sender() ! "pong"
}
}Supervision
Supervision definiert, wie Fehler behandelt werden. Resume: mit gleichem Zustand fortfahren. Restart: mit frischem Zustand neu erstellen. Stop: permanent terminieren. Escalate: den Parent handhaben lassen. OneForOneStrategy betrifft nur das fehlgeschlagene Child. AllForOneStrategy betrifft alle Children.
class Supervisor extends Actor {
override val supervisorStrategy = OneForOneStrategy() {
case _: ArithmeticException => Resume // Continue
case _: NullPointerException => Restart // Restart actor
case _: Exception => Stop // Stop actor
}
def receive = { case _ => }
}Router
Router verteilen Messages an mehrere Actors. Pool erstellt und verwaltet Worker. Group verwendet vorhandene Actors. Strategien: RoundRobin, Random, SmallestMailbox, ConsistentHashing, ScatterGatherFirst. Router verbessern Throughput durch Parallelisieren von Arbeit.
// Pool router: creates workers
val router = system.actorOf(
RoundRobinPool(5).props(Props[Worker]),
"router"
)
router ! "work" // Distributed to one of 5 workers
// Group router: uses existing actors
val group = system.actorOf(
RoundRobinGroup(paths).props(),
"group"
)Persistenz
PersistentActor speichert Events in einem Journal (Event Sourcing). Commands werden validiert, dann als Events persistiert. Bei Restart werden Events via receiveRecover replayed. Zustand wird aus Events wiederhergestellt. Ermöglicht Recovery nach Crashes. Verwenden Sie EventSourcedBehavior für Akka Typed.
class Counter extends PersistentActor {
var count = 0
override def persistenceId = "counter-1"
override def receiveCommand = {
case "inc" => persist(Incremented) { event =>
count += 1
}
}
override def receiveRecover = {
case Incremented => count += 1
}
}Verwandte Scala-Snippets
Copy-paste ready code for common tasks.
Pattern Matching
Werte destrukturieren und Fälle in Scala matchen.
Case Classes
Unveränderliche Datenklassen mit automatisch generierten equals/hashCode/toString.
Collection-Operationen
Funktionale Collection-Operationen: map, filter, fold, groupBy.
Traits und Mixins
Verhalten mit Traits und Standardimplementierungen komponieren.
Futures und Async
Asynchrone Berechnung mit Future und ExecutionContext.
Implicits (Given/Using in Scala 3)
Typklassen-Ableitung und Kontextübergabe via Implicits.
Akka Actors (Pekko)
Nebenläufigkeit durch Message-Passing mit dem Actor-Modell.
Typklassen (Cats-Stil)
Ad-hoc-Polymorphismus via Typklassen.
Was this helpful?