Blog

Funktionale Programmierung in Scala

Dennis Vriend

Aktualisiert Oktober 21, 2025
11 Minuten

Im vorigen Artikel über funktionale Programmierung in Python, , habe ich erklärt, dass funktionale Programmierung (FP) ein Paradigma ist, bei dem ein Programm aus Funktionen zusammengesetzt ist. Das Lösen von Problemen auf funktionale Weise führt zu einfachen, aber leistungsstarken Verarbeitungspipelines. Erstellen Sie Pipelines mit funktionalen Datenstrukturen (FDS),Funktionen höherer Ordnung (HoF) und Funktionen. Pipelines sind eine Kette von Funktionen, die immer einen Wert zurückgeben. Dieses Mal werden wir uns ansehen, wie man Verarbeitungspipelines mit Scala erstellt, einer funktionalen/objektorientierten Programmiersprache, die von Martin Odersky an der EPFL, einer weltweit führenden Universität in Lausanne, Schweiz, entwickelt wurde.

Funktional/Objektorientiert

Was bedeutet Funktional/Objektorientiert (FOO)? Sowohl die funktionale Programmierung als auch die objektorientierte Programmierung (OO) sind Paradigmen.FP verwendet die als primären Baustein. Größere Probleme können in kleine Funktionen zerlegt werden, diezusammengesetzt werden können, um ein größeres Problem zu lösen. Wie Sie Anwendungen mit Hilfe von Funktionen entwerfen, wird in dem Buch Funktionale Programmierung in Scala von Paul Chiusano und Runar Bjarnason. Bei OO ist der primäre Baustein ein object. Ein Objekt hat einen Zustand und ein Verhalten. Größere Probleme können in kleine Objekte zerlegt werden. Objekte kommunizieren miteinander, indem sie Nachrichten senden. Wie Sie Anwendungen mit Hilfe von Objekten entwerfen, wird in dem Buch Design Patterns beschrieben : Elements of Reusable Object-Oriented Software von Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides. Die Autoren des Buches werden oft als Gang of Four bezeichnet, abgekürzt als .Die meisten OO-Programmiersprachen unterstützen FP. Das liegt daran, dass eine Funktion als Objekt mit einer .apply() Methode ausgedrückt werden kann, die einen Wert zurückgibt. Um die Menge der zu tippenden Zeremonien und Zeichen zu reduzieren, ist es besser, eine Programmiersprache zu verwenden, die Funktionsliterale wie Java unterstützt, Python, Groovy, Typescript, JavaScript, Go oder Scala. Scala unterstützt sowohl OO als auch FP, da es FDS-Strukturen bietet - eine Datenstruktur mit Verhalten höherer Ordnung. FDS-Strukturen sind auch selbst Funktionen, so dass sie komponiert werden können. Das Zusammensetzen von Objekten auf funktionale Weise ermöglicht neue Entwurfsmuster, wie z.B. Verarbeitungspipelines. Schauen wir uns einmal an, wie das in Scala funktioniert!

Funktionen und Scala

Scala verwendet die Pfeil-Syntax =>, um eine Funktion zu definieren. Wenn wir zum Beispiel val f = (x: Int) => x + 1 eingeben, können wir eine Funktion namens f definieren, die, wenn sie auf einen Wert angewendet wird, x + 1 zurückgibt.

@ val f = (x: Int) => x + 1
f: Int => Int

@ f(1)
res1: Int = 2

// a shorter syntax
@ val f = (_: Int) + 1
f: Int => Int

@ f(1)
res2: Int = 2

Funktion Zusammensetzung

Bei der Funktionskomposition geht es um die Kombination von Funktionen. Eine kombinierte Funktion hat die Berechnungseigenschaften beider Funktionen. Wenn wir zum Beispiel zwei Funktionen f und g definieren, können wir sie zu einer Funktion h zusammensetzen, wobei h die Eigenschaften beider Funktionen besitzt.

@ val f = (_: Int) + 1
f: Int => Int

@ f(1)
res1: Int = 2

@ val g = (_: Int) + 2
g: Int => Int

@ val h = f compose g
h: Int => Int

@ h(1)
res2: Int = 4

Reine und unreine Werte

FP beginnt zu glänzen, wenn es mit Werten arbeitet, die eine bestimmte Art von Wert ausdrücken. Die meisten Programme arbeiten mit reinen Werten. Beispiele für reine Werte sind die Zahl 1, der Text hello, oder ein Wert wie true oder false. Mit FP können Sie das Ergebnis einer Berechnung als Wert ausdrücken. Ein solcher Wert ist unrein, weil er eine Auswirkung einer Berechnung ausdrückt. Beispiele für unreine Werte sind Success(123), Failure('First name is empty'), Some(1) oder None.

@ import scala.util._
import scala.util._

@ Success(123)
res1: Success[Int] = Success(123)

@ Failure(new RuntimeException("First name is empty"))
res2: Failure[Nothing] = Failure(java.lang.RuntimeException: First name is empty)

@ Some(1)
res3: Some[Int] = Some(1)

@ None
res4: None.type = None

Funktionale Datenstrukturen

Scala hat eine Menge FDS-Strukturen eingebaut, wie Option, Try. Wie Funktionen kann eine FDS komponiert werden, was bedeutet, dass wir eine FDS erstellen können, die das Verhalten von beiden hat. Scala verwendet den Namen flatMap, um zwei FDS-Strukturen zu komponieren.

@ import scala.util._
import scala.util._

@ val x = Option(1)
x: Option[Int] = Some(1)

@ Option(1).map(_ + 1)
res1: Option[Int] = Some(2)

@ Option(2).flatMap(x => y.map(_ + x))
res2: Option[Int] = Some(4)

@ Option.empty[Int].flatMap(x => y.map(_ + x))
res3: Option[Int] = None

@ Try(1)
res4: Try[Int] = Success(1)

@ Try(1/0)
res5: Try[Int] = Failure(java.lang.ArithmeticException: / by zero)

Die Grundprämisse von FP lautet: Wenn wir unseren Problemraum in benutzerdefinierte FDS-Strukturen zerlegen können, erhalten wir, wenn wir sie zusammensetzen, unsere vollständige Anwendung. Dieses Muster wird in dem Buch Functional Reactive Domain Modeling von Debasish Ghosh beschrieben.

Funktionen höherer Ordnung

Bevor wir Pipelines erstellen können, müssen wir uns mit Funktionen höherer Ordnung (HoF) befassen. HoF sind Funktionen, die eine Funktion als Argument erhalten oder eine Funktion als Ergebnis zurückgeben. Ein einfaches Konzept, das wir bereits aus der Funktionskomposition kennen.

@ val f = (_: Int) + 1
f: Int => Int

@ val g = (_: Int) + 2
g: Int => Int

@ val h = (x: Int => Int, y: Int => Int) => x compose y
h: (Int => Int, Int => Int) => Int => Int

@ val i = h(f, g)
i: Int => Int

@ i(1)
res1: Int = 4

Operationen auflisten

Eine Liste ist ebenfalls eine FDS-Struktur. Sie drückt den Effekt aus, dass sie null oder mehr Elemente hat. Die Liste unterstützt Operationen, die eine Funktion als Argument akzeptieren, ist aber selbst auch eine Funktion.

@ List(1, 2, 3).map(_ + 1)
res0: List[Int] = List(2, 3, 4)

@ List(1, 2, 3).filter(_ > 1)
res1: List[Int] = List(2, 3)

@ List(1, 2, 3).flatMap(x => List(x + 1, x + 2, x + 3))
res2: List[Int] = List(2, 3, 4, 3, 4, 5, 4, 5, 6)

@ List.empty[Int].map(_ + 1)
res3: List[Int] = List()

@ List(1, 2, 3).fold(0)(_ + _)
res4: Int = 6

@ List(1, 2, 3).sum
res5: Int = 6

@ List("the", "book", "is", "green").mkString(",")
res6: String = "the,book,is,green"

@ List(1, 2, 3).headOption
res7: Option[Int] = Some(1)

@ List(1, 2, 3).partition(_ > 1)
res8: (List[Int], List[Int]) = (List(2, 3), List(1))

// a list is also a function as it can be applied
@ List(1, 2, 3).apply(0)
res9: Int = 1

Der Listen-FDS gibt immer einen Wert zurück. Je nachdem, welche Pipeline wir erstellen, gibt die Liste einen unreinen oder einen reinen Wert zurück. Um einen reinen Wert zu erstellen, verwenden Sie oder . Um unreine Werte zurückzugeben, verwenden Sie map, flatMap oder headOption.

Eine Liste von Optionswerten

Wenn wir eine Liste mit unreinen Werten haben, können wir eine Pipeline erstellen, um die unreinen Werte zu kombinieren. Das Ergebnis ist ebenfalls ein unreiner Wert.

// notice the list contains the values 1, 2, and 3
@ val xs = List(Option(1), Option(2), Option.empty[Int], Option(3)).flatten
res1: List[Int] = List(1, 2, 3)

@ xs.sum
res2: Int = 6

Zum Beispiel drückt eine List of Option, geschrieben als List[Option], eine Liste optionaler Werte aus. Diese Liste könnte das Ergebnisder Validierung von Benutzereingaben sein. Die dritte Benutzereingabe ist leer, aber wir wollen sie weiterverarbeiten, also verwenden wir die Operation, um die Wirkung des optionalen Wertes zu entfernen. Wir haben nun eine Liste mit reinen Werten, die wir bearbeiten können.

Eine Liste der Auswirkungen

Manchmal möchten wir die Gesamtwirkung einer Liste von unreinen Werten kennen. Die Operation, die wir verwenden, heißt sequence, aber dieses Konzept wird von der Scala-Standardbibliothek nicht unterstützt. Wir können die Operation zu Scala hinzufügen, indem wir eine Bibliothek namens scalaz importieren.

@ import $ivy.org.scalaz::scalaz-core:7.2.7, scalaz._, Scalaz._

@ val xs = List(Option(1), Option(2), Option.empty[Int], Option(3))
xs: List[Option[Int]] = List(Some(1), Some(2), None, Some(3))

@ xs.sequence
res1: Option[List[Int]] = None

@ val xs = List(Option(1), Option(2),Option(3))
xs: List[Option[Int]] = List(Some(1), Some(2), Some(3))

@ val ys = xs.sequence
ys: Option[List[Int]] = Some(List(1, 2, 3))

@ ys.fold(0)(_.sum)
res6: Int = 6

Wenn wir eine Liste mit optionalen Werten haben, wollen wir wissen, ob ein Wert fehlt, und wenn ja, wollen wir nicht, dass weiter verarbeitet wird. Die Operation sequence für die Liste gibt uns den Wert None zurück, wenn ein Wert fehlt. Wenn alle Werte vorhanden sind, wird der Wert Some(List(1, 2, 3)) zurückgegeben. Bei der Operation fold kann ich wählen, ob eine Null zurückgeben soll, wenn die Liste leer ist, oder die Summe aller Werte, wenn die Liste nicht leer ist.

Werte validieren

Der FDS Validation eignet sich hervorragend für die Überprüfung von Benutzereingaben. In Kombination mit können wir fehlende Werte ausdrücken.In Kombination mit Regex validiert es Benutzereingaben. Die Scala-Standardbibliothek unterstützt die Validierung von Benutzereingaben nicht. Wir können Validation zu Scala hinzufügen, indem wir eine Bibliothek namens scalaz importieren.

@ import $ivy.org.scalaz::scalaz-core:7.2.7, scalaz._, Scalaz._

@ import scala.util.matching._
import scala.util.matching._

@ Option.empty[Int].toSuccessNel("No value")
res1: ValidationNel[String, Int] = Failure(NonEmpty[No value])

@ Option(1).toSuccessNel("No value")
res2: ValidationNel[String, Int] = Success(1)

@ Validation.lift(-20)(_ <= 0, "Number should be positive")
res3: Validation[String, Int] = Failure("Number should be positive")

@ Validation.lift("a")(("d+".r).findFirstIn(_).isEmpty, "Input must be a number")
res4: Validation[String, String] = Failure("Input must be a number")

@ Validation.lift("1")(("d+".r).findFirstIn(_).isEmpty, "Input must be a number")
res5: Validation[String, String] = Success("1")

Eine Liste von Überprüfungswerten

Wenn wir eine Liste von Überprüfungsergebnissen haben, können wir die Auswirkungen kombinieren und eine Verarbeitungspipeline erstellen, die alle Fehler zusammenfasst oder eine Liste von Werten zurückgibt, die zurückgegeben werden sollen. Die Operation, die wir verwenden werden, heißt sequenceU und ist Teil von scalaz.

@ import $ivy.org.scalaz::scalaz-core:7.2.7, scalaz._, Scalaz._

@ val fn = Option.empty[String].toSuccessNel("Firstname is empty")
fn: ValidationNel[String, String] = Failure(NonEmpty[Firstname is empty])

@ val ln = Option.empty[String].toSuccessNel("Lastname is empty")
ln: ValidationNel[String, String] = Failure(NonEmpty[Lastname is empty])

@ val age = Option("27").toSuccessNel("Age is empty")
age: ValidationNel[String, String] = Success("27")

@ val zipcode = Option.empty[String].toSuccessNel("Invalid zipcode")
zipcode: ValidationNel[String, String] = Failure(NonEmpty[Invalid zipcode])

@ val validated = results.sequenceU
validated: Validation[NonEmptyList[String], List[String]] = Failure(NonEmpty[Firstname is empty,Lastname is empty,Invalid zipcode])

validated.fold(_.toList, identity).mkString(",")
res1: String = "Firstname is empty,Lastname is empty,Invalid zipcode"

Da wir Fehler haben, sammelt die Operation sequenceU alle Fehler und gibt sie in einer Liste zurück. Wenn wir Erfolgswerte haben, können wir sie wie folgt verarbeiten:

@ import $ivy.org.scalaz::scalaz-core:7.2.7, scalaz._, Scalaz._

@ val fn = Option("Dennis").toSuccessNel("Firstname is empty")
fn: ValidationNel[String, String] = Success("Dennis")

@ val ln = Option("Vriend").toSuccessNel("Lastname is empty")
ln: ValidationNel[String, String] = Success("Vriend")

@ val age  = Option("43").toSuccessNel("Age is empty")
age: ValidationNel[String, String] = Success("43")

@ val validated = List(fn, ln, age).sequenceU
validated: Validation[NonEmptyList[String], List[String]] = Success(List("Dennis", "Vriend", "43"))                    ^                    ^

@ val result = validated.fold(_.toList, identity)
result: List[String] = List("Dennis", "Vriend", "43")

Verarbeitung von AWS Java SDK-Fehlern Funktionaler Stil

Der Validierungs-FDS fängt Ausnahmen ab, die z.B. durch das AWS Java SDK ausgelöst werden. Verwenden Sie die HoF. Java löst Ausnahmen aus, wenn Operationen fehlschlagen. Beim Löschen eines Buckets kann die Operation fehlschlagen. Validation fängt den Fehler als Validierungswert ab. Der Listen-FDS fasst alle Fehler zusammen.

@ import $ivy.org.scalaz::scalaz-core:7.2.7, scalaz._, Scalaz._

@ import $ivy.com.amazonaws:aws-java-sdk:1.11.362, com.amazonaws.services.s3._, com.amazonaws.services.s3.model._, com.amazonaws.event._

@ val client = AmazonS3ClientBuilder.defaultClient()
client: AmazonS3

@ val results = List("foo", "bar", "baz").map(bucketName => Validation.fromTryCatchNonFatal(client.deleteBucket(bucketName)).leftMap(_.getMessage.wrapNel))
results: List[Validation[NonEmptyList[String], Unit]] = List(
  Failure(
    NonEmpty[The bucket is in this region: us-east-1. Please use this region to retry the request (Service: Amazon S3; Status Code: 301; Error Code: PermanentRedirect; Request ID: 41D03FA4E55C4D11; S3 Extended Request ID: aW+TQWmyxLWoa+zUPMU1ml7iTvoYpITKxW9tywlgvgJQS99lwvtdxH9Q8KRxQFFVkBXSaV63vcw=)]
  ),
  Failure(
    NonEmpty[The bucket is in this region: us-east-1. Please use this region to retry the request (Service: Amazon S3; Status Code: 301; Error Code: PermanentRedirect; Request ID: F8D9EE5008E28EC0; S3 Extended Request ID: uYcSr9e/S55eOsX5PyYE9HEBg/nRA1WHVmJdo72IXr4hZsYZC4zcMq8Y/kYbEuCBV1YELoLy0fE=)]
  ),
  Failure(
    NonEmpty[The bucket is in this region: us-east-1. Please use this region to retry the request (Service: Amazon S3; Status Code: 301; Error Code: PermanentRedirect; Request ID: 513BA210C54FCAE9; S3 Extended Request ID: 3GAvErrQY+kzLFl43k+wgra16LvG4BPdjCKJW/YdlwR//bsucdgYXGsTxtzLETa9iKtDl+YjfCM=)]
  )
)

@ results.sequenceU.fold(_.toList, _ => List.empty[String]).mkString(",")
res12: String = "The bucket is in this region: us-east-1. Please use this region to retry the request (Service: Amazon S3; Status Code: 301; Error Code: PermanentRedirect; Request ID: 41D03FA4E55C4D11; S3 Extended Request ID: aW+TQWmyxLWoa+zUPMU1ml7iTvoYpITKxW9tywlgvgJQS99lwvtdxH9Q8KRxQFFVkBXSaV63vcw=),The bucket is in this region: us-east-1. Please use this region to retry the request (Service: Amazon S3; Status Code: 301; Error Code: PermanentRedirect; Request ID: F8D9EE5008E28EC0; S3 Extended Request ID: uYcSr9e/S55eOsX5PyYE9HEBg/nRA1WHVmJdo72IXr4hZsYZC4zcMq8Y/kYbEuCBV1YELoLy0fE=),The bucket is in this region: us-east-1. Please use this region to retry the request (Service: Amazon S3; Status Code: 301; Error Code: PermanentRedirect; Request ID: 513BA210C54FCAE9; S3 Extended Request ID: 3GAvErrQY+kzLFl43k+wgra16LvG4BPdjCKJW/YdlwR//bsucdgYXGsTxtzLETa9iKtDl+YjfCM=)"

Fazit

Das Erstellen von Verarbeitungspipelines ist in Scala sehr einfach. Scala unterstützt sowohl FP als auch OO und wird mit Standard-FDS-Strukturen geliefert. Einige erweiterte FDS-Strukturen müssen aus Bibliotheken wie Scalaz importiert werden. Mit ein paar Zeilen Code können wir leistungsstarke Verarbeitungspipelines formulieren, die leicht zu verstehen sind. Ich verwende diese Art der Problemlösung sehr oft. FP spart mir Zeit, reduziert die Komplexität und führt zu modularem Code. Ich kann meinen Code jederzeit testen und Funktionen in anderen Einstellungen wiederverwenden.

Verfasst von

Dennis Vriend

Contact

Let’s discuss how we can support your journey.