Mauw! Begin nu met het gebruik van Cats in uw project

Zachte inleiding tot de kattenbibliotheek.

Invoering

Cats is een bibliotheek met abstracties voor functioneel programmeren in Scala.

Er zijn een aantal geweldige posts en cursussen over katten op internet (zoals Herding cats en een tutorial over Scala-oefeningen), maar ze hebben de neiging om de categorieën / typeklassen die in de bibliotheek zijn geïmplementeerd te verkennen in plaats van praktische kant-en-klare- gebruik voorbeelden van het gebruik van katten in bestaande codebases. Deze blogpost schetst nauwelijks het oppervlak van wat Cats kan doen, maar biedt in plaats daarvan een beknopte praktische introductie tot de patronen waarvan u waarschijnlijk het meest zult profiteren in uw Scala-project. Als u dagelijks monaden zoals Future of Option gebruikt, is het zeer waarschijnlijk dat Cats de leesbaarheid van uw code kunnen vereenvoudigen en verbeteren.

Raadpleeg de Cats-wiki op GitHub voor richtlijnen over het toevoegen van de bibliotheek aan uw projectafhankelijkheden. We houden ons aan versie 0.9.0 in het hele bericht.

Laten we de bibliotheek per pakket doornemen en kijken naar de syntaxis die in elk pakket beschikbaar is.

Helpers voor Option en beide

cats.syntax.option._ importeren

Door dit pakket te importeren, wordt obj.some syntaxis ingeschakeld - gelijk aan Some (obj). Het enige echte verschil is dat de waarde al is geüpgraded naar optie [T] van Some [T].

Het gebruik van obj.some in plaats van Some (obj) kan soms de leesbaarheid van unit-tests verbeteren. Als u bijvoorbeeld de volgende impliciete klasse toevoegt aan uw BaseSpec, TestHelper of hoe uw basisklasse voor tests ook wordt genoemd:

dan kun je de onderstaande geketende syntax gebruiken (ervan uitgaande dat je unit-tests gebaseerd zijn op scalamock; zie ook een bericht van Bartosz Kowalik):

Dat is leesbaarder dan Future.successful (sommige (gebruiker)), vooral als dit patroon zich vaak herhaalt in de testreeks. Keten.

none [T] is op zijn beurt stenografie voor Option.empty [T], die gewoon None is, maar al upcast is van None.typeto Option [T]. Als u een meer gespecialiseerd type opgeeft, helpt de Scala-compiler soms het type uitdrukkingen dat Geen bevat.

katten.syntax.either._ importeren

obj.asRight is Right (obj), obj.asLeft is Left (obj). In beide gevallen wordt het type geretourneerde waarde verbreed van rechts of links naar een van beide. Net zoals bij .some zijn deze helpers handig te combineren met .asFuture om de leesbaarheid van unit-tests te verbeteren:

Either.fromOption (optie: Optie [A], ifNone: => E) is op zijn beurt een nuttige hulp voor het omzetten van een optie in een van beide. Als de opgegeven optie Some (x) is, wordt deze Right (x). Anders wordt het links met de opgegeven ifNone-waarde.

instantiespakketten en cartesiaanse syntaxis

cats.instances importeren. ._

Er zijn een paar typeklassen die van fundamenteel belang zijn voor katten (en in het algemeen voor categorie-gebaseerde functionele programmering), de belangrijkste zijn Functor, Applicative en Monad. We gaan niet veel in op deze blogpost (zie bijv. De al genoemde tutorial), maar wat belangrijk is om te weten, is dat om de meeste syntaxis van Cats te gebruiken, je ook de impliciete typeklasse-instanties moet importeren voor de structuren die je ' opnieuw werken met.

Gewoonlijk is het net voldoende om het juiste cats.instances-pakket te importeren. Wanneer u bijvoorbeeld de transformaties op futures uitvoert, moet u cats.instances.future._ importeren. De bijbehorende pakketten voor opties en lijsten worden cats.instances.option._ en cats.instances.list._ genoemd. Ze bieden de impliciete typeklasse-instanties die de syntaxis van Cats correct moet werken.

Als een kanttekening, als u problemen ondervindt bij het vinden van de vereiste instanties of het syntax-pakket, is de snelle oplossing gewoon katten.implicits._ te importeren. Dit is echter geen voorkeursoplossing, omdat het de compilatietijd aanzienlijk kan verhogen - vooral als het in veel bestanden in het project wordt gebruikt. Het wordt algemeen als een goede praktijk beschouwd om beperkte invoer te gebruiken om een ​​deel van de impliciete afwikkelingslast van de samensteller te ontlasten.

cats.syntax.cartesian._ importeren

Het cartesiaanse pakket biedt | @ | syntaxis, die een intuïtieve constructie mogelijk maakt voor het toepassen van een functie die meer dan één parameter nodig heeft voor meerdere effectieve waarden (zoals futures).

Laten we zeggen dat we 3 futures hebben, een van het type Int, een van het type String, een van het type User en een methode die drie parameters accepteert - Int, String en User.

Ons doel is om de functie toe te passen op de waarden die worden berekend door die 3 futures. Met cartesiaanse syntaxis wordt dit zeer eenvoudig en beknopt:

Zoals eerder aangegeven, om de impliciete instantie te bieden (namelijk Cartesiaans [Future]) vereist voor | @ | om goed te werken, moet je cats.instances.future._ importeren.

Dit bovenstaande idee kan nog korter worden uitgedrukt, alleen:

Het resultaat van de bovenstaande uitdrukking is van het type Future [ProcessingResult]. Als een van de geketende futures faalt, zal de resulterende toekomst ook mislukken met dezelfde uitzondering als de eerste falende toekomst in de keten (dit is fail-fast gedrag). Wat belangrijk is, alle futures lopen parallel, in tegenstelling tot wat er zou gebeuren in een onbegrip:

In het bovenstaande fragment (dat zich onder de motorkap vertaalt naar flatMap en kaartaanroepen), zal stringFuture niet worden uitgevoerd totdat intFuture met succes is voltooid en op dezelfde manier zal userFuture alleen worden uitgevoerd nadat stringFuture is voltooid. Maar omdat de berekeningen onafhankelijk van elkaar zijn, is het perfect haalbaar om ze parallel met | @ | uit te voeren in plaats daarvan.

doorkruisen

cats.syntax.traverse._ importeren

traverse

Als je een instantie obj van het type F [A] hebt die kan worden toegewezen (zoals Future) en een functieplezier van het type A => G [B], dan zou het oproepen van obj.map (fun) je F [G [ B]]. In veel voorkomende praktijkgevallen, zoals wanneer F Option is en G Future is, zou je Option [Future [B]] krijgen, wat waarschijnlijk niet is wat je wilde.

traverse biedt hier een oplossing. Als je traverse in plaats van map aanroept, zoals obj.traverse (leuk), krijg je G [F [A]], wat in ons geval toekomstig [optie [B]] zal zijn; dit is veel nuttiger en gemakkelijker te verwerken dan Optie [Toekomst [B]].

Als kanttekening, er is ook een speciale methode Future.traverse in het metgezelobject Future, maar de Cats-versie is veel leesbaarder en kan gemakkelijk werken aan elke structuur waarvoor bepaalde typeklassen beschikbaar zijn.

volgorde

volgorde vertegenwoordigt een nog eenvoudiger concept: er kan worden gedacht dat de typen eenvoudig worden verwisseld van F [G [A]] naar G [F [A]] zonder zelfs de ingesloten waarde in kaart te brengen zoals traverse doet.

obj.sequence is in feite in Cats geïmplementeerd als obj.traverse (identiteit). Aan de andere kant is obj.traverse (fun) ongeveer hetzelfde als obj.map (fun) .sequence.

flatTraverse

Als je een obj van het type F [A] hebt en een functieplezier van het type A => G [F [B]], dan levert obj.map (f) het resultaat op van het type F [G [F [B]]] - zeer onwaarschijnlijk wat je wilde.

Het verplaatsen van de obj in plaats van in kaart brengen helpt een beetje - je krijgt in plaats daarvan G [F [F [B]]. Aangezien G meestal iets is als Future en F List of Option is, zou je eindigen met Future [Option [Option [A]] of Future [List [List [A]]] - een beetje lastig om te verwerken.

De oplossing zou kunnen zijn om het resultaat in kaart te brengen met een _.flatten-aanroep zoals:

en op deze manier krijgt u aan het einde het gewenste type G [F [B]].

Er is echter een handige snelkoppeling voor deze flatTraverse genaamd:

en dat lost ons probleem voorgoed op.

Monad-transformatoren

import cats.data.OptionT

Een exemplaar van OptionT [F, A] kan worden gezien als een wrapper over F [Optie [A]] die een aantal nuttige methoden toevoegt die specifiek zijn voor geneste typen die niet beschikbaar zijn in F of Option zelf. Meestal is uw F Future (of soms gladde DBIO, maar dit vereist een implementatie van Cats-type klassen zoals Functor of Monad voor DBIO). Wrappers zoals OptionT staan ​​algemeen bekend als monad-transformatoren.

Een vrij gebruikelijk patroon is het toewijzen van de binnenwaarde opgeslagen in een instantie van F [Optie [A]] aan een instantie van F [Optie [B]] met een functie van type A => B. Dit kan worden gedaan met een vrij uitgebreide syntaxis net zoals:

Met OptionT kan dit als volgt worden vereenvoudigd:

De bovenstaande kaart retourneert een waarde van het type OptionT [Future, String].

Om de onderliggende waarde Future [Option [String]] te krijgen, roept u eenvoudig .value aan op de OptionT-instantie. Het is ook een haalbare oplossing om volledig over te schakelen naar OptionT [Future, A] in methodeparameter / retourtypen en de Future [Option [A]] in typeverklaringen volledig (of bijna volledig) weg te gooien.

Er zijn verschillende manieren om een ​​OptionT-instantie te maken. De methodekoppen in de onderstaande tabel zijn enigszins vereenvoudigd: de typeparameters en typeklassen die voor elke methode zijn vereist, worden overgeslagen.

In productiecode gebruikt u meestal de syntaxis van OptionT (...) om een ​​exemplaar van Future [Option [A]] in Option [F, A] te verpakken. De andere methoden blijken op hun beurt nuttig om door Optie getypte nepwaarden in eenheidsproeven in te stellen.

We zijn al een van de methoden van OptionT tegengekomen, namelijk map. Er zijn verschillende andere methoden beschikbaar en deze verschillen meestal door de handtekening van de functie die ze als parameter accepteren. Zoals het geval was met de vorige tabel, worden de verwachte typeklassen overgeslagen.

In de praktijk zult u waarschijnlijk map en semiflatMap gebruiken.

Zoals altijd het geval is met flatMap en map, kunt u het niet alleen expliciet gebruiken, maar ook onder de motorkap in onbegrip, zoals in het onderstaande voorbeeld:

De OptionT [Future, Money] -instantie geretourneerd door getReservedFundsForUser zal een Niet-waarde toevoegen als een van de drie samengestelde methoden een OptionT retourneert die overeenkomt met Geen. Als het resultaat van alle drie oproepen Sommige bevat, bevat de uiteindelijke uitkomst ook Sommige.

cats.data.EitherT importeren

EitherT [F, A, B] is de monadetransformator voor Ofwel - je kunt het beschouwen als een wrapper over een F [Ofwel [A, B]] waarde.

Net als in de bovenstaande sectie, heb ik de methodekoppen vereenvoudigd, typeparameters overslaan of hun contextgrenzen en ondergrenzen.

Laten we even kijken hoe we een EitherT-instantie kunnen maken:

Ter verduidelijking: EitherT.fromWrapt de opgegeven Either in F, terwijl EitherT.right en EitherT.left de waarde respectievelijk binnen de gegeven F inRight en Left wikkelen. Ofwel T.pure, op zijn beurt, verpakt de gegeven B-waarde in Rechts en vervolgens in F.

Een andere handige manier om een ​​EitherT-instantie te bouwen, is door de methoden van OptionT te gebruiken voor Links en Rechts:

toRight is redelijk analoog aan de methode Either.fromOption eerder vermeld: net als vanOptionbuilt een of van een Option, toRight maakt een EitherT van een OptionT. Als de oorspronkelijke OptionTstores Sommige waarde, wordt deze ingepakt in Rechts; anders wordt de waarde die is opgegeven als parameter left in een Left gewikkeld.

toLeft is de tegenhanger van toRight die de waarde Some in Links verpakt en Geen omzet in Rightenclosing van de opgegeven juiste waarde. Dit wordt in de praktijk minder vaak gebruikt, maar kan b.v. voor het afdwingen van unieke controles in code. We geven Links terug als de waarde is gevonden en Rechts als deze nog niet bestaat in het systeem.

De beschikbare methoden in EitherT lijken veel op de methoden die we in OptionT hebben gezien, maar er zijn enkele opvallende verschillen. U kunt in het begin misschien in verwarring raken als het gaat om b.v. kaart. In het geval van OptionT was het vrij duidelijk wat er moest worden gedaan: map moet de optie in de Future gaan en vervolgens de bijgevoegde Option zelf in kaart brengen. Dit is iets minder duidelijk in het geval van EitherT: moet het in kaart worden gebracht over zowel de waarden Links als Rechts of alleen de waarde Rechts?

Het antwoord is dat EitherT een juiste voorkeur heeft, daarom wordt in gewone kaarten eigenlijk de juiste waarde gebruikt. Dit is anders dan in de Scala-standaardbibliotheek tot 2.11, wat op zijn beurt onbevooroordeeld is: er is geen kaart beschikbaar in Either, alleen voor de linker- en rechterprojecties.

Dat gezegd hebbende, laten we snel kijken naar de juiste bevooroordeelde methoden die EitherT [F, A, B] biedt:

Als een kanttekening, er zijn ook bepaalde methoden in EitherT (die je waarschijnlijk op een gegeven moment waarschijnlijk nodig zult hebben) die in kaart brengen over de waarde Links, zoals leftMap, of over zowel waarden Links als Rechts, zoals vouwen of bimap.

EitherT is erg handig voor mislukte kettingverificaties:

In het bovenstaande voorbeeld voeren we verschillende controles een voor een uit voor het item. Als een van de controles mislukt, bevat de resulterende EitherT een waarde Links. Anders, als alle cheques een recht opleveren (natuurlijk bedoelen we een recht verpakt in een van beide), dan zal het uiteindelijke resultaat ook recht bevatten. Dit is een faalsnel gedrag: we stoppen effectief de stroom voor begrip bij het eerste Left-achtige resultaat.

Als u in plaats daarvan op zoek bent naar validatie die de fouten verzamelt (bijvoorbeeld bij het omgaan met door de gebruiker verstrekte formuliergegevens), zijn cats.data.Validated een goede keuze.

Gebruikelijke problemen

Als iets niet naar verwachting wordt gecompileerd, zorg er dan eerst voor dat alle vereiste implicaties voor katten binnen het bereik vallen - probeer gewoon cats.implicits._ te importeren en kijk of het probleem blijft bestaan. Zoals eerder vermeld, is het echter beter om smalle import te gebruiken, maar als de code niet compileert, is het soms de moeite waard om alleen de hele bibliotheek te importeren om te controleren of het probleem is opgelost.

Als u Futures gebruikt, zorg dan voor een impliciete ExecutionContext in het bereik, anders zal Cats geen impliciete instanties kunnen afleiden voor de klassen van Future type.

De compiler kan vrij vaak problemen hebben met het afleiden van parameters van het type voor traversemethoden en sequentiemethoden. Een voor de hand liggende oplossing is om deze typen direct op te geven, zoals list.traverse [Future, Unit] (leuk). Dit kan in bepaalde gevallen echter behoorlijk uitgebreid worden en de betere manier is om de equivalente methoden traverseU en sequenceU, zoals list.traverseU (leuk), te proberen. Ze doen wat bedrog op type niveau (met katten. Helaas, vandaar de U) om de compiler te helpen de typeparameters af te leiden.

IntelliJ meldt soms fouten in door katten geladen code, ook al gaat de bron onder scalac. Een voorbeeld hiervan zijn aanroepen van de methoden van cats.data.Nested class, die correct compileren onder scalac, maar typ geen controle onder de presentatiecompiler van IntelliJ. Het zou zonder problemen moeten werken onder Scala IDE.

Als advies voor uw toekomstige leerproces: de applicatieve typeklasse is, ondanks de sleutelbetekenis in functioneel programmeren, enigszins moeilijk te begrijpen. Naar mijn mening is het veel minder intuïtief dan Functor of Monad, ook al staat het eigenlijk precies tussen Functor en Monad in de erfenishiërarchie. De beste aanpak om te begrijpen Applicatief is om eerst te begrijpen hoe een product (dat een F [A] en F [B] transformeert naar een F [(A, B)]) werkt in plaats van zich te concentreren op de ietwat exotische ap-operatie zelf.