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),
Funktional/Objektorientiert
Was bedeutet Funktional/Objektorientiert (FOO)? Sowohl die funktionale Programmierung als auch die objektorientierte Programmierung (OO) sind Paradigmen.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 .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 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 Ergebnis
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
@ 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
@ 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
Unsere Ideen
Weitere Blogs
Contact




