9 Tipps zur Verwendung der Katzenbibliothek in Scala

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:

  1. Es ist kompakter und verständlicher (da die Reihenfolge der Methodenaufrufe gespeichert ist).
  2. Im Gegensatz zu Konstruktoroptionen werden die Rückgabetypen dieser Methoden auf einen Supertyp erweitert, d.h.

 import cats.implicits._ Some("a") //Some[String] "a".some //Option[String] 

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 //Right(b) success2 *> success1 //Right(a) success1 *> failure //Left(400) failure *> success1 //Left(400) 

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 //Right(a) 

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) //EitherT(Some(Right(a))) EitherT.liftF(none[String]) //EitherT(None) 

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) //Some(jak się masz!) liftedIntMatcher(0) //None intMatcher(1) //jak się masz! intMatcher(0) //Exception in thread "main" scala.MatchError: 0 

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:

 // where t2: Tuple2[F[A0], F[A1]] def mapN[Z](f: (A0, A1) => Z)(implicit functor: Functor[F], semigroupal: Semigroupal[F]): F[Z] = Semigroupal.map2(t2._1, t2._2)(f) 

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(_ ++ _) //Some(ab) (List(1, 2), List(3, 4), List(0, 2).mapN(_ * _ * _)) //List(0, 6, 0, 8, 0, 12, 0, 16) 

Vergessen Sie übrigens nicht, dass Sie bei Katzen eine Karte und eine leftmap für Tupel erhalten:

 ("a".some, List("b","c").mapN(_ ++ _)) //won't compile, because outer type is not the same ("a".some, List("b", "c")).leftMap(_.toList).mapN(_ ++ _) //List(ab, ac) 

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._ //interchangable with eg Monix's Task type Query[T] = IO[Option[T]] def defineMead(qName: Query[String], qHoneyRatio: Query[Double], qAgingYears: Query[Double]): Query[Mead] = (for { name <- OptionT(qName) honeyRatio <- OptionT(qHoneyRatio) agingYears <- OptionT(qAgingYears) } yield Mead(name, honeyRatio, agingYears)).value def defineMead2(qName: Query[String], qHoneyRatio: Query[Double], qAgingYears: Query[Double]): Query[Mead] = for { name <- qName honeyRatio <- qHoneyRatio agingYears <- qAgingYears } yield (name, honeyRatio, agingYears).mapN(Mead) 

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 //Some(Right(aaa)) 

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") //"Got error code 400" 

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] //Right[Int, String](a) 

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] //Some(a) 

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:

 //  ,   `pure`, //    import cats.implicits._ import cats.instances.option._ "a".pure[Option] //could not find implicit value for parameter F: cats.Applicative[Option] 

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.

Source: https://habr.com/ru/post/de448128/


All Articles