Die funktionale Programmierung in Scala kann aufgrund einiger syntaktischer und semantischer Merkmale der Sprache schwierig zu beherrschen sein. Insbesondere einige der Sprachwerkzeuge und -methoden zur Implementierung der von Ihnen geplanten Aufgaben mithilfe der Hauptbibliotheken scheinen offensichtlich zu sein, wenn Sie mit ihnen vertraut sind. Zu Beginn des Studiums, insbesondere allein, ist es jedoch nicht so einfach, sie zu erkennen.
Aus diesem Grund habe ich beschlossen, dass es nützlich ist, einige funktionale Programmiertipps in Scala zu teilen. Beispiele und Namen entsprechen Katzen, aber die Syntax in Scalaz sollte aufgrund der allgemeinen theoretischen Grundlage ähnlich sein.

9) Konstruktoren der Erweiterungsmethode
Beginnen wir mit den vielleicht grundlegendsten Tool-Erweiterungsmethoden aller Art, die eine Instanz in Option, Entweder usw. verwandeln, insbesondere:
.some
und die entsprechende none
Konstruktor-Methode für Option
;.asRight
, .asLeft
für .asLeft
;.valid
, .invalid
, .validNel
, .invalidNel
für Validated
Zwei Hauptvorteile ihrer Verwendung:
- Es ist kompakter und verständlicher (da die Reihenfolge der Methodenaufrufe gespeichert ist).
- Im Gegensatz zu Konstruktoroptionen werden die Rückgabetypen dieser Methoden auf einen Supertyp erweitert, d.h.
import cats.implicits._ Some("a")
Obwohl sich die Typinferenz im Laufe der Jahre verbessert hat und die Anzahl möglicher Situationen, in denen dieses Verhalten dem Programmierer hilft, ruhig zu bleiben, abgenommen hat, sind Kompilierungsfehler aufgrund übermäßig spezialisierter Typisierung in Scala heute noch möglich. Sehr oft entsteht bei der Arbeit mit
Either
der Wunsch, den Kopf gegen einen Tisch zu
Either
(siehe
Scala with Cats, Kapitel 4.4.2).
Noch etwas zum Thema:
.asRight
und
.asLeft
noch einen Typparameter. Beispiel:
"1".asRight[Int]
ist
Either[Int, String]
. Wenn dieser Parameter nicht angegeben wird, versucht der Compiler, ihn auszugeben und
Nothing
. Trotzdem ist es bequemer, als jedes Mal beide Parameter anzugeben oder auch nicht, wie im Fall von Konstruktoren.
8) Fünfzig Farben *>
Der in einer
Apply
Methode definierte *> -Operator (
Monad
in
Applicative
,
Monad
usw.) bedeutet einfach "die anfängliche Berechnung verarbeiten und das Ergebnis durch das ersetzen, was im zweiten Argument angegeben ist". In der Code-Sprache (im Fall von
Monad
):
fa.flatMap(_ => fb)
Warum sollte ein obskurer symbolischer Operator für eine Operation verwendet werden, die keinen spürbaren Effekt hat? Wenn Sie ApplicativeError und / oder MonadError verwenden, werden Sie feststellen, dass der Vorgang den Fehlereffekt für den gesamten Workflow beibehält. Nehmen Sie
Either
als Beispiel:
import cats.implicits._ val success1 = "a".asRight[Int] val success2 = "b".asRight[Int] val failure = 400.asLeft[String] success1 *> success2
Wie Sie sehen, bleibt die Berechnung auch im Fehlerfall kurzgeschlossen. *> hilft Ihnen bei der Arbeit mit verzögerten Berechnungen in
Monix
,
IO
und dergleichen.
Es gibt eine symmetrische Operation <*. Im Fall des vorherigen Beispiels:
success1 <* success2
Wenn Ihnen die Verwendung von Symbolen fremd ist, müssen Sie nicht darauf zurückgreifen. *> Ist nur ein Alias für
productR
und * <ist ein Alias für
productL
.
Hinweis
In einem persönlichen Gespräch bemerkte Adam Warski (danke, Adam!) Zu Recht, dass es neben *> (
productR
) auch >> von
FlatMapSyntax
. >> wird wie
fa.flatMap(_ => fb)
, jedoch mit zwei Nuancen:
- Es wird unabhängig von
productR
definiert. Wenn sich daher aus irgendeinem Grund der Vertrag dieser Methode ändert (theoretisch kann es geändert werden, ohne die monadischen Gesetze zu verletzen, aber ich bin mir bei MonadError
nicht sicher), werden Sie nicht leiden. - was noch wichtiger ist, >> hat einen zweiten Operanden, der durch Call-by-Name aufgerufen wird, d.h.
fb: => F[B]
. Der Unterschied in der Semantik wird grundlegend, wenn Sie Berechnungen durchführen, die zu einer Stapelexplosion führen können.
Aus diesem Grund habe ich *> häufiger verwendet. Vergessen Sie auf die eine oder andere Weise nicht die oben aufgeführten Faktoren.
7) Segel setzen!
Viele nehmen sich Zeit, um das
lift
in den Kopf zu bekommen. Aber wenn Sie Erfolg haben, werden Sie feststellen, dass er überall ist.
Wie viele Begriffe, die in der Luft der funktionalen Programmierung aufsteigen, stammt der
lift
aus der
Kategorietheorie . Ich werde versuchen zu erklären: Nehmen Sie eine Operation vor und ändern Sie die Signatur ihres Typs so, dass sie in direktem Zusammenhang mit dem abstrakten Typ F steht.
In Cats ist das einfachste Beispiel
Functor :
def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f)
Dies bedeutet: Ändern Sie diese Funktion so, dass sie auf den angegebenen Funktortyp F wirkt.
Die Lift-Funktion ist häufig gleichbedeutend mit verschachtelten Konstruktoren für einen bestimmten Typ. Also ist
EitherT.liftF
im Wesentlichen
EitherT.right.
Beispiel aus Scaladoc :
import cats.data.EitherT import cats.implicits._ EitherT.liftF("a".some)
Kirsche auf dem Kuchen:
lift
überall in der Scala-Standardbibliothek vorhanden. Das beliebteste (und vielleicht nützlichste Beispiel für die tägliche Arbeit) ist
PartialFunction
:
val intMatcher: PartialFunction[Int, String] = { case 1 => "jak się masz!" } val liftedIntMatcher: Int => Option[String] = intMatcher.lift liftedIntMatcher(1)
Jetzt können wir zu dringlicheren Themen übergehen.
6) mapN
mapN
ist eine nützliche
mapN
für die Arbeit mit Tupeln. Auch dies ist keine Neuheit, sondern ein Ersatz für den guten alten Operator
|@|
Er ist ein Schrei.
So sieht mapN bei einem Tupel aus zwei Elementen aus:
Im Wesentlichen können wir Werte innerhalb eines Tupels von jedem F abbilden, das eine Halbgruppe (Produkt) und ein Funktor (Zuordnung) ist. Also:
import cats.implicits._ ("a".some, "b".some).mapN(_ ++ _)
Vergessen Sie übrigens nicht, dass Sie bei Katzen eine Karte und eine
leftmap
für Tupel erhalten:
("a".some, List("b","c").mapN(_ ++ _))
Eine weitere nützliche
.mapN
Funktion ist das Instanziieren von
.mapN
:
case class Mead(name: String, honeyRatio: Double, agingYears: Double) ("półtorak".some, 0.5.some, 3d.some).mapN(Mead) //Some(Mead(półtorak,0.5,3.0))
Natürlich verwenden Sie hierfür lieber den for-Schleifenoperator, aber mapN vermeidet in einfachen Fällen monadische Transformatoren.
import cats.effect.IO import cats.implicits._
Methoden haben ähnliche Ergebnisse, aber letztere verzichten auf monadische Transformatoren.
5) Verschachtelt
Nested
ist im Wesentlichen ein verallgemeinertes Doppel von Monadentransformatoren. Wie der Name schon sagt, können Sie unter bestimmten Bedingungen Anhangsvorgänge ausführen. Hier ist ein Beispiel für
.map(_.map( :
import cats.implicits._ import cats.data.Nested val someValue: Option[Either[Int, String]] = "a".asRight.some Nested(someValue).map(_ * 3).value
Zusätzlich zu
Functor
verallgemeinert
Nested
Applicative
,
ApplicativeError
und
Traverse
. Weitere Informationen und Beispiele finden Sie
hier .
4) .recover / .recoverWith / .handleError / .handleErrorWith / .valueOr
Die funktionale Programmierung in Scala hat viel mit der Behandlung des Fehlereffekts zu tun.
ApplicativeError
und
MonadError
verfügen über einige nützliche Methoden, und es kann hilfreich sein, die subtilen Unterschiede zwischen den vier Hauptmethoden herauszufinden. Also, mit
ApplicativeError F[A]:
handleError
konvertiert alle Fehler am handleError
gemäß der angegebenen Funktion in A.recover
funktioniert auf ähnliche Weise, akzeptiert jedoch Teilfunktionen und kann daher von Ihnen ausgewählte Fehler in A konvertieren.handleErrorWith
ähnelt handleError
, das Ergebnis sollte jedoch wie F[A]
aussehen. Dies bedeutet, dass Sie Fehler konvertieren können.recoverWith
wie "Wiederherstellen", erfordert jedoch als Ergebnis auch F[A]
.
Wie Sie sehen, können
handleErrorWith
auf die
handleErrorWith
von
handleErrorWith
und
recoverWith
, die alle möglichen Funktionen abdecken. Jede Methode hat jedoch ihre Vorteile und ist auf ihre Weise bequem.
Im Allgemeinen empfehle ich Ihnen, sich mit der
ApplicativeError- API vertraut zu machen, die eine der reichsten an Katzen ist und von MonadError geerbt wird. Dies bedeutet, dass sie in
cats.effect.IO
,
monix.Task
usw. unterstützt wird.
Es gibt eine andere Methode für
Either/EitherT
,
Validated
und
Ior
-
.valueOr
. Im Wesentlichen funktioniert es wie
.getOrElse
für
Option
, ist jedoch generisch für Klassen, die etwas „links“ enthalten.
import cats.implicits._ val failure = 400.asLeft[String] failure.valueOr(code => s"Got error code $code")
3) Gassenkatzen
Gassenkatzen sind eine bequeme Lösung für zwei Fälle:
- Fälle von Kachelklassen, die ihren Gesetzen nicht zu 100% entsprechen;
- Ungewöhnliche Hilfstypklassik, die richtig verwendet werden kann.
Historisch gesehen ist die Monadeninstanz für
Try
beliebteste in diesem Projekt, da
Try
, wie Sie wissen, nicht alle monadischen Gesetze in Bezug auf schwerwiegende Fehler erfüllt. Jetzt ist er wirklich mit Katzen bekannt.
Trotzdem empfehle ich Ihnen, sich mit
diesem Modul vertraut zu machen. Es scheint Ihnen nützlich zu sein.
2) Importe verantwortungsbewusst behandeln
Sie müssen aus der Dokumentation, dem Buch oder von einem anderen Ort wissen, dass Katzen eine bestimmte Importhierarchie verwenden:
cats.x
für grundlegende (Kernel-) Typen;
cats.data
für Datentypen wie Validiert, Monadentransformatoren usw.;
cat.syntax.x._ zur Unterstützung von Erweiterungsmethoden, damit Sie sth.asRight, sth.pure usw. aufrufen können;
cats.instances.x.
_ um die Implementierung verschiedener Typklassen direkt in den impliziten Bereich für einzelne spezifische Typen zu importieren, sodass beim Aufrufen von beispielsweise sth.pure der Fehler "implizit nicht gefunden" nicht auftritt.
Natürlich haben Sie den Import von
cats.implicits._
bemerkt, der die gesamte Syntax und alle Instanzen der Typklasse im impliziten Bereich importiert.
Grundsätzlich sollten Sie bei der Entwicklung mit Cats mit einer bestimmten Reihenfolge von Importen aus den FAQ beginnen, nämlich:
import cats._ import cats.data._ import cats.implicits._
Wenn Sie die Bibliothek besser kennenlernen, können Sie sie nach Ihrem Geschmack kombinieren. Befolgen Sie eine einfache Regel:
cats.syntax.x
bietet eine Erweiterungssyntax für x.cats.instances.x
bietet cats.instances.x
.
Wenn Sie beispielsweise
.asRight
benötigen, eine Erweiterungsmethode für
Either
, gehen Sie wie folgt vor:
import cats.syntax.either._ "a".asRight[Int]
Um
Option.pure
zu erhalten,
Option.pure
Sie jedoch
cats.syntax.monad
AND cats.instances.option
:
import cats.syntax.applicative._ import cats.instances.option._ "a".pure[Option]
Durch manuelles Optimieren Ihres Imports begrenzen Sie implizite Bereiche in Ihren Scala-Dateien und reduzieren dadurch die Kompilierungszeit.
Bitte tun Sie dies jedoch nicht, wenn die folgenden Bedingungen nicht erfüllt sind:
- Sie haben Cats bereits gut gemeistert
- Ihr Team besitzt die Bibliothek auf derselben Ebene
Warum? Weil:
Dies liegt daran, dass sowohl
cats.implicits
als auch
cats.instances.option
Erweiterungen von
cats.instances.OptionInstances
. Tatsächlich importieren wir den impliziten Bereich zweimal, als wir den Compiler verwirren.
Darüber hinaus gibt es keine Magie in der Hierarchie der Impliziten - dies ist eine klare Folge von Typerweiterungen. Sie müssen sich nur auf die Definition von
cats.implicits
beziehen und die
cats.implicits
untersuchen.
Für 10-20 Minuten können Sie es genug studieren, um Probleme wie diese zu vermeiden - glauben Sie mir, diese Investition wird sich definitiv auszahlen.
1) Vergessen Sie nicht die Katzen-Updates!
Sie denken vielleicht, Ihre FP-Bibliothek ist zeitlos, aber tatsächlich werden
cats
und
scalaz
aktiv aktualisiert. Nehmen Sie als Beispiel Katzen. Hier sind nur die neuesten Änderungen:
Vergessen Sie daher bei der Arbeit mit Projekten nicht, die Bibliotheksversion zu überprüfen, die Hinweise für neue Versionen zu lesen und rechtzeitig zu aktualisieren.