Mjau! Begynn å bruke Cats i prosjektet ditt akkurat nå

Skånsom introduksjon til Cats bibliotek.

Introduksjon

Cats er et bibliotek som gir abstraksjoner for funksjonell programmering i Scala.

Det er et par gode innlegg og kurs om katter der ute på nettet (som Herding-katter og en tutorial om Scala-øvelser), men de har en tendens til å utforske kategoriene / typeklassene som er implementert i biblioteket, i stedet for å gi praktiske klar-til- bruk eksempler på hvordan du bruker katter i eksisterende kodebaser. Dette blogginnlegget klør knapt overflaten av hva Cats kan gjøre, men gir i stedet en kortfattet praktisk introduksjon til mønstrene du mest sannsynlig vil dra nytte av i Scala-prosjektet ditt. Hvis du bruker noen monader som Future eller Option på daglig basis, er det veldig sannsynlig at Katter kan forenkle og forbedre lesbarheten til koden din.

Vennligst referer til Cats wiki på GitHub for retningslinjer for hvordan du legger biblioteket til prosjektavhengighetene dine. Vi holder oss til versjon 0.9.0 i hele innlegget.

La oss gå gjennom biblioteket pakkevis, se på syntaksen som er tilgjengelig i hver pakke.

Hjelpere for alternativ og enten

importere cats.syntax.option._

Å importere denne pakken muliggjør obj.some syntaks - tilsvarer noen (obj). Den eneste virkelige forskjellen er at verdien allerede er oppdatert til alternativ [T] fra noen [T].

Å bruke obj.some i stedet for Noen (obj) kan noen ganger forbedre lesbarheten til enhetstester. Hvis du for eksempel legger til følgende implisitte klassen til BaseSpec, TestHelper eller hva baseklassen din for tester heter:

så kan du bruke den lenede syntaks som vist nedenfor (forutsatt at enhetstestene dine er basert på skalamock; se også et innlegg av Bartosz Kowalik):

Det er mer leselig enn Future.suksess (Noen (bruker)), spesielt hvis dette mønsteret gjentas ofte i testsuiten. Å lenke. Somme.som fremtid på slutten i stedet for å sette den foran, hjelper også å fokusere på hva som faktisk blir returnert fremfor på den forventede innpakningstypen.

none [T] er på sin side kortfattet for Option.empty [T] som bare er Ingen, men allerede oppdatert fra None.typeto Option [T]. Tilveiebringelse av en mer spesialisert type hjelper noen ganger Scala-kompilatoren til riktig uttrykk for typen uttrykk som inneholder Ingen.

importere cats.syntax.either._

obj.asRight is Right (obj), obj.asLeft is Left (obj). I begge tilfeller utvides den returnerte verdien fra høyre eller venstre til enten. Akkurat som tilfellet var med. Noen er disse hjelperne nyttige å kombinere med .asFuture for å forbedre lesbarheten til enhetstester:

Either.fromOption (alternativ: Alternativ [A], ifNone: => E) er på sin side en nyttig hjelper for å konvertere et alternativ til et enten. Hvis det oppgitte alternativet er Noen (x), blir det riktig (x). Ellers blir det Venstre med den oppgitte ifNone-verdien inne.

forekomster pakker og kartesisk syntaks

importer katter. tilfeller. ._

Det er et par typer klasser som er grunnleggende for katter (og generelt for kategoribasert funksjonell programmering), de viktigste er Functor, Applicative og Monad. Vi skal ikke gå så mye inn i dette blogginnlegget (se f.eks. Den allerede nevnte veiledningen), men det som er viktig å vite er at for å bruke de fleste syntaks av Cats, må du også importere implisitte typeklasseinstanser for strukturene du har. opererer med.

Vanligvis er det akkurat nok å importere den aktuelle cats.instances-pakken. For eksempel, når du gjør transformasjoner på futures, må du importere cats.instances.future._. De tilsvarende pakkene for alternativer og lister kalles cats.instances.option._ og cats.instances.list._. De gir implisitte forekomster av typen klasse som syntaks av katter trenger å fungere ordentlig.

Som en merknad, hvis du har problemer med å finne de nødvendige forekomster eller syntakspakke, er den raske løsningen å bare importere cats.implicits._. Dette er imidlertid ikke en foretrukket løsning, ettersom den kan øke samlingen ganger betydelig - spesielt hvis den brukes i mange filer over hele prosjektet. Det anses generelt som god praksis å bruke smal import for å fjerne noe av den implisitte oppløsningsbyrden fra kompilatoren.

importere cats.syntax.cartesian._

Den kartesiske pakken gir | @ | syntaks, som gir mulighet for en intuitiv konstruksjon for å bruke en funksjon som tar mer enn en parameter til flere effektive verdier (som futures).

La oss si at vi har 3 futures, en av typen Int, en av typen streng, en av typen bruker og en metode som godtar tre parametere - Int, streng og bruker.

Målet vårt er å bruke funksjonen til verdiene beregnet av disse 3 futures. Med kartesisk syntaks blir dette veldig enkelt og konsist:

Som påpekt før, for å gi den implisitte forekomsten (nemlig Cartesian [Future]) som kreves for | @ | For å fungere ordentlig, bør du importere cats.instances.future._.

Denne ideen over kan uttrykkes enda kortere, bare:

Resultatet av uttrykket ovenfor vil være av typen Future [ProcessingResult]. Hvis noen av de lenede futures mislykkes, vil den resulterende fremtiden også mislykkes med samme unntak som den første sviktende fremtiden i kjeden (dette er fail-fast oppførsel). Det som er viktig, alle futures vil løpe parallelt, i motsetning til hva som ville skje i en forforståelse:

I ovennevnte kodestykke (som under panseret oversettes til flatMap og kartanrop), kjører stringFuture ikke før intFuture er fullført, og på samme måte kjøres userFuture bare etter at stringFuture er fullført. Men siden beregningene er uavhengige av hverandre, er det perfekt levedyktig å kjøre dem parallelt med | @ | i stedet.

traversering

importere cats.syntax.traverse._

traversere

Hvis du har en forekomstobj av type F [A] som kan kartlegges (som Future) og en funksjon som er morsom av type A => G [B], vil det å ringe obj.map (moro) gi deg F [G [ B]]. I mange vanlige tilfeller i det virkelige liv, som når F er Option og G er Future, ville du fått Option [Future [B]], som mest sannsynlig ikke er det du ønsket.

traverse kommer som en løsning her. Hvis du ringer traverse i stedet for kart, som obj.traverse (moro), får du G [F [A]], som vil være Future [Alternativ [B]] i vårt tilfelle; dette er mye mer nyttig og enklere å behandle enn alternativ [Future [B]].

Som en sideanmerkning er det også en dedikert metode Future.traverse i Future-følgesvenn-objektet, men Cats-versjonen er langt mer lesbar og kan enkelt arbeide med hvilken som helst struktur som bestemte typeklasser er tilgjengelige for.

sekvens

sekvens representerer et enda enklere konsept: det kan tenkes å bare bytte typene fra F [G [A]] til G [F [A]] uten å kartlegge den lukkede verdien som travers gjør.

obj.sequence er faktisk implementert i Cats som obj.traverse (identitet). På den annen side tilsvarer obj.traverse (moro) omtrent det som obj.map (moro) .sequence.

flatTraverse

Hvis du har et objekt av type F [A] og en funksjon som er morsomt av type A => G [F [B]], så gjør obj.map (f) resultatet av typen F [G [F [B]]] - veldig usannsynlig å være det du ønsket.

Å krysse obj i stedet for å kartlegge hjelper litt - du får G [F [F [B]] i stedet. Siden G vanligvis er noe som Future og F er List or Option, vil du ende opp med Future [Alternativ [Alternativ [A]] eller Future [List [List [A]]] - litt vanskelig å behandle.

Løsningen kan være å kartlegge resultatet med en _.flatten samtale som:

og på denne måten får du ønsket type G [F [B]] på slutten.

Imidlertid er det en fin snarvei for denne kalt flatTraverse:

og det løser problemet vårt for godt.

Monad-transformatorer

importer katter.data.OptionT

En forekomst av OTT [F, A] kan tenkes å være en innpakning over F [Alternativ [A]] som legger til et par nyttige metoder som er spesifikke for nestede typer som ikke er tilgjengelige i F eller Option selv. Vanligvis er din F fremtid (eller noen ganger slick's DBIO, men dette krever implementering av Cats type klasser som Functor eller Monad for DBIO). Pakkere som OptionT er generelt kjent som monad-transformatorer.

Et ganske vanlig mønster er å kartlegge den indre verdien som er lagret i en forekomst av F [Alternativ [A]] til en forekomst av F [Alternativ [B]] med en funksjon av type A => B. Dette kan gjøres med ganske ordinær syntaks som:

Med bruk av OptionT kan dette forenkles som følger:

Kartet over vil returnere en verdi av typen OptionT [Future, String].

For å få den underliggende Future [Option [String]] -verdien, bare ring .value på OptionT-forekomsten. Det er også en levedyktig løsning å fullstendig bytte til OptionT [Future, A] i metodeparameter / returtyper og helt (eller nesten helt) grøft Future [Option [A]] i typedeklarasjoner.

Det er flere måter å konstruere en OptionT-forekomst på. Metodeoverskriftene i tabellen nedenfor er litt forenklet: typeparametrene og typeklassene som kreves av hver metode, hoppes over.

I produksjonskode bruker du ofte OptionT (...) -syntaks for å pakke en forekomst av Future [Option [A]] inn i Option [F, A]. De andre metodene viser seg på sin side nyttige å sette opp OptionT-typede mock-verdier i enhetstester.

Vi har allerede kommet over en av OptionTs metoder, nemlig kart. Det er flere andre metoder som er tilgjengelige, og de skiller seg stort sett ut fra signaturen til funksjonen de godtar som parameter. Som tilfellet var med forrige tabell, blir de forventede typeklassene hoppet over.

I praksis er det mest sannsynlig at du bruker kart og semiflatMap.

Som alltid er tilfelle med flatMap og kart, kan du bruke det ikke bare eksplisitt, men også under panseret i forforståelser, som i eksemplet nedenfor:

Alternativet [Future, Money] -instansen som returneres av getReservedFundsForUser, vil omslutte en ikke-verdi hvis noen av de tre sammensatte metodene returnerer et alternativT som tilsvarer Ingen. Ellers, hvis resultatet av alle tre samtalene inneholder noen, vil det endelige resultatet også inneholde noen.

importer katter.data.EtterT

EntenT [F, A, B] er monadtransformatoren for enten - du kan tenke på det som en innpakning over en F [Enten [A, B]] -verdi.

Akkurat som i avsnittet ovenfor, forenklet jeg metodehodene, hoppetype-parametere eller deres kontekstgrenser og lavere grenser.

La oss ta en rask titt på hvordan du oppretter en EitherT-forekomst:

Bare for å avklare: Enten fra hver side pakker den medfølgende enten inn i F, mens enten EntenT.right og EitherT.left pakker verdien inne i den medfølgende F til henholdsvis høyre og venstre. EntenT.pure innpakker på sin side den medfølgende B-verdien til høyre og deretter i F.

En annen nyttig måte å konstruere en EitherT-forekomst på er å bruke OptionTs metoder for å hente og rette:

toRight er ganske analogt med metoden Either.fromOption som er nevnt før: akkurat som fromOptionbuilt an Enither from an Option, toRight oppretter en EitherT fra en OptionT. Hvis den opprinnelige OptionTstores Noen verdi, vil den bli pakket inn til høyre; Ellers vil verdien som er gitt som venstre parameter bli pakket inn i en Venstre.

toLeft er toRights motpart som omslutter verdien til noen og forvandler ingen til å høyre lukke den oppgitte høyre verdien. Dette brukes mindre ofte i praksis, men kan tjene f.eks. for å håndheve unikhetskontroller i kode. Vi returnerer Venstre hvis verdien er funnet, og Høyre hvis den ennå ikke eksisterer i systemet.

Metodene som er tilgjengelige i EntenT, er ganske like de vi har sett i OptionT, men det er noen markante forskjeller. Du kan komme i en viss forvirring med det første når det gjelder f.eks. kart. Når det gjelder OptionT, var det ganske åpenbart hva som skulle gjøres: kart skulle gå over alternativet som er vedlagt i Future, og deretter kartlegge det vedlagte alternativet. Dette er litt mindre opplagt i tilfelle av enten enten: skal det kartlegge både Venstre- og høyre-verdiene, eller bare riktig verdi?

Svaret er at EitherT er rett-partisk, derfor omhandler rent kart faktisk den riktige verdien. Dette er i motsetning til enten i Scala-standardbiblioteket opp til 2.11, som igjen er objektiv: det er ikke noe kart tilgjengelig i enten, bare for projeksjonene til venstre og høyre.

Når det er sagt, la oss raskt se på de riktige partiske metodene som enten enten [F, A, B] tilbyr:

Som en sideanmerkning er det også visse metoder i EntenT (som du sannsynligvis vil trenge på et tidspunkt) som kartlegger over Venstre-verdien, for eksempel leftMap, eller over både venstre og høyre verdier, som fold eller bimap.

EntenT er veldig nyttig for feiling-hurtigkjedede bekreftelser:

I eksemplet over kjører vi forskjellige sjekker mot varen én etter én. Hvis noen av sjekkene mislykkes, vil den resulterende enten-verdien inneholde en Venstre-verdi. Ellers, hvis alle sjekkene gir en rettighet (vi mener selvfølgelig en rettighet innpakket i et enten-alternativ), vil det endelige utfallet også inneholde rettighet. Dette er en hurtig-hurtig oppførsel: vi stopper effektivt for forståelsesflyten ved det første Venstre-resultatet.

Hvis du i stedet leter etter validering som akkumulerer feilene (f.eks. Når du håndterer skjemadata fra brukeren), kan cats.data.Validated være et godt valg.

Vanlige problemer

Hvis noe ikke samles som forventet, må du først forsikre deg om at alle de nødvendige kattene-implikasjonene er i omfanget - bare prøv å importere cats.implicits._ og se om problemet vedvarer. Som nevnt før er det riktignok bedre å bruke smal import, men hvis koden ikke samler, er det noen ganger verdt å bare importere hele biblioteket for å sjekke om det løser problemet.

Hvis du bruker Futures, må du sørge for å gi en implisitt ExecutionContext i omfanget, ellers vil ikke katter kunne utlede implisitte forekomster for Future's type klasser.

Kompilatoren kan ganske ofte ha problemer med å utlede parametere for travers og sekvensmetoder. En åpenbar løsning er å spesifisere disse typene direkte, for eksempel list.traverse [Future, Unit] (moro). Dette kan imidlertid bli ganske ordinært i visse tilfeller, og den bedre måten er å prøve likeverdige metoder traverseU og sequU, som list.traverseU (moro). De gjør noen lurerier på typnivå (med katter. Bare derav U) for å hjelpe kompilatoren med å utlede typeparametrene.

IntelliJ rapporterer noen ganger feil i katter-lastet kode selv om kilden passerer under skalasje. Et slikt eksempel er påkallinger av metodene til katter.data.Nestet klasse, som samles riktig under skala, men ikke skriver inn sjekk under IntelliJs presentasjonskompilator. Det skal fungere uten problemer under Scala IDE.

Som et råd for din fremtidige læring: Den anvendelige typen klassen, til tross for det er sentralt i funksjonell programmering, er litt vanskelig å forstå. Etter min mening er det mye mindre intuitivt enn Functor eller Monad, selv om det faktisk står rett mellom Functor og Monad i arvehierarkiet. Den beste tilnærmingen for å forstå applikativ er å først forstå hvordan produkt (som forvandler en F [A] og F [B] til en F [(A, B)]) snarere enn å fokusere på den noe eksotiske ap-operasjonen.