tämä artikkeli oli alun perin liite Reactive Programming with RxJava Bookissa. Kuitenkin johdanto monadeihin, vaikkakin se liittyi hyvin paljon reaktiiviseen ohjelmointiin, ei sopinut kovin hyvin. Niinpä päätin ottaa sen esille ja julkaista tämän erikseen blogikirjoituksena. Olen tietoinen siitä, että” ikioma, puoliksi oikea ja puoliksi täydellinen selitys monadeille ”on uusi” Hei, maailma ” ohjelmointiblogeissa. Silti artikkelissa tarkastellaan funktoreita ja monadeja Java-tietorakenteiden ja kirjastojen tietynlaisesta kulmasta. Siksi ajattelin, että kannattaa jakaa.
RxJava suunniteltiin ja rakennettiin hyvin perustavien käsitteiden, kuten funktorien, monoidien ja monadien päälle. Vaikka RX mallinnettiin alun perin imperatiiviselle C# – kielelle ja opettelemme rxjavaa, työskennellen samalla imperatiivin kielen päällä, kirjaston juuret ovat funktionaalisessa ohjelmoinnissa. Sinun ei pitäisi olla yllättynyt, kun ymmärrät, kuinka kompakti RxJava API on. On melko paljon vain kourallinen ydinluokkia, tyypillisesti muuttumaton, ja kaikki koostuu käyttäen enimmäkseen puhtaita toimintoja.
funktionaalisen ohjelmoinnin (tai funktionaalisen tyylin) viimeaikaisen nousun myötä, joka yleisimmin ilmenee nykyaikaisissa kielissä kuten Scalassa tai Clojuressa, monadeista tuli laajalti keskusteltu aihe. Niiden ympärillä on paljon kansanperinnettä:
monadi on endofunktorien luokkaan kuuluva monoidi, mikä on ongelma?
James Iry
monadin kirous on se, että kun saa loppiaisen, kun ymmärtää – ”Oh that’ s what it is” – menettää kyvyn selittää sitä kenelle tahansa.
Douglas Crockford
valtaosa ohjelmoijista, erityisesti ne, joilla ei ole toiminnallista ohjelmointitaustaa, on taipuvaisia uskomaan monadien olevan jokin hämärä tietojenkäsittelytieteen käsite, niin teoreettinen, ettei se voi mitenkään auttaa heidän ohjelmointiurallaan. Tämä negatiivinen näkökulma voi johtua siitä, että kymmenet artikkelit ja blogikirjoitukset ovat joko liian abstrakteja tai liian kapeita. Mutta käy ilmi, että monadeja on kaikkialla ympärillämme, jopa tavallisessa Java-kirjastossa, varsinkin kun Java Development Kit (JDK) 8 (lisää siitä myöhemmin). Nerokasta on se, että kun ymmärtää monadeja ensimmäistä kertaa, yhtäkkiä useat toisiinsa liittymättömät luokat ja abstraktiot, jotka palvelevat täysin eri tarkoituksia, tulevat tutuiksi.
monadit yleistävät erilaisia näennäisesti itsenäisiä käsitteitä niin, että Uuden monadin inkarnaation oppiminen vie hyvin vähän aikaa. Esimerkiksi sinun ei tarvitse oppia, miten CompletableFuture toimii Java 8: ssa – kun huomaat, että se on monad, tiedät tarkalleen, miten se toimii ja mitä voit odottaa sen semantiikasta. Ja sitten kuulet RxJava joka kuulostaa niin paljon erilainen, mutta koska Observable on monad, ei ole paljon lisättävää. On olemassa lukuisia muita esimerkkejä monads olet jo törmännyt tietämättä, että. Siksi tämä osio on hyödyllinen kertaus, vaikka et itse käytä Rxjavaa.
Funktorit
ennen kuin selitämme, mikä monadi on, tutkitaan yksinkertaisempaa konstruktiota, jota kutsutaan funktoriksi . Funktor on tyypitetty tietorakenne, joka kiteyttää joitakin arvoja. Syntaktisesta näkökulmasta funktori on kontti, jolla on seuraava API:
import java.util.function.Function;interface Functor<T> { <R> Functor<R> map(Function<T, R> f);}
pelkkä syntaksi ei kuitenkaan riitä ymmärtämään, mikä funktori on. Ainoa functorin tarjoama operaatio on kartta (), joka ottaa funktion f. tämä funktio vastaanottaa laatikon sisällä olevan, muuntaa sen ja käärii tuloksen-is: nä toiseksi functoriksi. Lue se huolellisesti. Functor<t> on aina muuttumaton kontti, joten kartta ei koskaan mutatoi alkuperäistä kohdetta, jolle se toteutettiin. Sen sijaan, se palauttaa tuloksen (tai tulokset – olla potilas) kääritty upouusi functor, mahdollisesti eri tyyppiä R. lisäksi functors ei pitäisi suorittaa mitään toimia, kun identiteetti toiminto on käytössä, että on kartta(x -> x). Tällaisen kuvion pitäisi aina palauttaa joko sama functor tai tasa-arvoinen instanssi.
usein Funktoria<T> verrataan laatikkoa pitävään t-ilmentymään, jossa ainoa tapa olla vuorovaikutuksessa tämän arvon kanssa on sen muuttaminen. Functorista ei kuitenkaan ole olemassa idiomaattista tapaa avata tai paeta. Arvo (t) pysyy aina funktorin yhteydessä. Miksi toiminnanharjoittajat ovat hyödyllisiä? Ne yleistävät useita yhteisiä idiomeja, kuten kokoelmia, lupauksia, optionals, jne. yhdellä yhtenäisellä API: lla, joka toimii kaikissa niissä. Haluan esitellä pari toiminnanharjoittajaa, jotka tekevät sinusta sujuvamman tämän API: n avulla.:
interface Functor<T,F extends Functor<?,?>> { <R> F map(Function<T,R> f);}class Identity<T> implements Functor<T,Identity<?>> { private final T value; Identity(T value) { this.value = value; } public <R> Identity<R> map(Function<T,R> f) { final R result = f.apply(value); return new Identity<>(result); }}
identiteetin kääntämiseen tarvittiin ylimääräinen F-tyypin parametri. Mitä näit edellisessä esimerkissä oli yksinkertaisin functor vain hallussaan arvo. Kaikki mitä voit tehdä, että arvo on muuttaa sen sisällä kartta menetelmä, mutta ei ole mitään keinoa purkaa sitä. Tätä pidetään puhtaan funktorin ulottumattomissa. Ainoa tapa vuorovaikutuksessa functor on soveltamalla sekvenssejä tyyppi-turvallinen muutoksia:
Identity<String> idString = new Identity<>("abc");Identity<Integer> idInt = idString.map(String::length);
tai sujuvasti, aivan kuten säveltää toimintoja:
Identity<byte> idBytes = new Identity<>(customer) .map(Customer::getAddress) .map(Address::street) .map((String s) -> s.substring(0, 3)) .map(String::toLowerCase) .map(String::getBytes);
tästä näkökulmasta kartoitus yli functor ei ole paljon erilainen kuin vain vetoamalla ketjutettu toimintoja:
byte bytes = customer .getAddress() .street() .substring(0, 3) .toLowerCase() .getBytes();
Miksi edes vaivautuisit sellaiseen sanalliseen kääreeseen, joka ei ainoastaan tuo mitään lisäarvoa, vaan ei myöskään kykene purkamaan sisältöä takaisin? NO, on käynyt ilmi, että voit mallintaa useita muita käsitteitä käyttämällä tätä raakaa functor-abstraktiota. Esimerkiksi alkaen Java 8 valinnainen on functor kanssa kartta () menetelmä. Toteuttakaamme se tyhjästä:
class FOptional<T> implements Functor<T,FOptional<?>> { private final T valueOrNull; private FOptional(T valueOrNull) { this.valueOrNull = valueOrNull; } public <R> FOptional<R> map(Function<T,R> f) { if (valueOrNull == null) return empty(); else return of(f.apply(valueOrNull)); } public static <T> FOptional<T> of(T a) { return new FOptional<T>(a); } public static <T> FOptional<T> empty() { return new FOptional<T>(null); }}
nyt siitä tulee mielenkiintoista. FOptional<T>
funktorilla voi olla arvo, mutta yhtä hyvin se voi olla tyhjä. Se on tyyppiturvallinen tapa koodata null
. On kaksi tapaa rakentaa FOptional
– toimittamalla arvo tai luomalla empty()
instanssi. Molemmissa tapauksissa, aivan kuten Identity
,FOptional
on muuttumaton ja voimme olla vuorovaikutuksessa arvon kanssa vain sisältä. Mikä eroaaFOptional
siitä, että muunnosfunktiota f
ei voida soveltaa mihinkään arvoon, jos se on tyhjä. Tämä tarkoittaa, että functor ei välttämättä kapseloi täsmälleen yhtä tyyppiä T
olevaa arvoa. Se voi yhtä hyvin kääriä mielivaltaisen määrän arvoja, aivan kuten List
… functor:
import com.google.common.collect.ImmutableList;class FList<T> implements Functor<T, FList<?>> { private final ImmutableList<T> list; FList(Iterable<T> value) { this.list = ImmutableList.copyOf(value); } @Override public <R> FList<?> map(Function<T, R> f) { ArrayList<R> result = new ArrayList<R>(list.size()); for (T t : list) { result.add(f.apply(t)); } return new FList<>(result); }}
API pysyy samana: otat funktorin muodonmuutoksessa-mutta käytös on paljon erilaista. Nyt sovellamme muutosta jokaisen kohteen FList, deklaratiivisesti muuttaa koko luettelon. Joten jos sinulla on lista asiakkaista ja haluat listan heidän kaduistaan, se on niin yksinkertaista kuin:
import static java.util.Arrays.asList;FList<Customer> customers = new FList<>(asList(cust1, cust2));FList<String> streets = customers .map(Customer::getAddress) .map(Address::street);
se ei ole enää niin yksinkertaista kuin sanoa customers.getAddress().street()
, et voi vedotagetAddress()
asiakaskokoelmaan, sinun täytyy vedota getAddress()
jokaiseen yksittäiseen asiakkaaseen ja sitten laittaa se takaisin kokoelmaan. Muuten, Groovy totesi tämän kuvion niin yleiseksi, että siinä on itse asiassa syntaksisokeri sille: customer*.getAddress()*.street()
. Tämä operaattori, joka tunnetaan nimellä spread-dot, on todellisuudessa map
in disguise. Ehkä ihmettelet, miksi iteroin list
käsin map
sisällä sen sijaan, että käyttäisin Stream
s Java 8:list.stream().map(f).collect(toList())
? Kuulostaako tutulta? Mitä jos kertoisinjava.util.stream.Stream<T>
Javassa on myös funktori? Ja muuten, myös monadin?
nyt pitäisi nähdä funktoreiden ensimmäiset edut-ne abstrahoivat sisäisen representaation pois ja tarjoavat johdonmukaisen, helppokäyttöisen API: n erilaisten tietorakenteiden päälle. Viimeisenä esimerkkinä Saanen esitellä lupauksen funktorin, joka muistuttaa Future
. Promise
”lupaa”, että arvo tulee käyttöön jonain päivänä. Sitä ei ole vielä olemassa, ehkä siksi, että jokin taustalaskenta on poikinut tai odotamme ulkopuolista tapahtumaa. Mutta se ilmestyy joskus tulevaisuudessa. Promise<T>
täyttämisen mekaniikka ei ole kiinnostava, mutta funktorin luonne on:
Promise<Customer> customer = //...Promise<byte> bytes = customer .map(Customer::getAddress) .map(Address::street) .map((String s) -> s.substring(0, 3)) .map(String::toLowerCase) .map(String::getBytes);
näyttääkö tutulta? Se on asian ydin! Functorin toteuttaminen ei kuulu tämän artiklan soveltamisalaan eikä edes ole tärkeää. Tarpeeksi sanoa, että olemme hyvin lähellä toteuttaa CompletableFuture alkaen Java 8 ja olemme melkein löytäneet havaittavissa RxJava. Mutta takaisin toiminnanharjoittajiin. Lupaus<asiakas> ei pidä asiakkaan arvoa ihan vielä. Sillä luvataan olevan tällainen arvo tulevaisuudessa. Mutta voimme silti kartoittaa tällaisen functorin, aivan kuten teimme FOptional ja FList – syntaksi ja semantiikka ovat täsmälleen samat. Käyttäytyminen seuraa sitä, mitä functor edustaa. Vetoan asiakkaaseen.kartta (asiakas:: getAddress) tuottaa lupauksen<osoite>, eli kartta on estoton. asiakas.kartta () asiakas lupaa täydentää. Sen sijaan se palauttaa toisen, toisenlaisen lupauksen. Kun upstream promise täydentyy, downstream promise soveltaa funktiota, joka välitetään kartalle () ja siirtää tuloksen alavirtaan. Yhtäkkiä meidän functor sallii meidän putki asynkronisia laskelmia ei-esto tavalla. Mutta sinun ei tarvitse ymmärtää tai oppia, että – koska lupaus on functor, sen on noudatettava syntaksia ja lakeja.
on olemassa monia muitakin suuria esimerkkejä funktoreista, jotka esimerkiksi esittävät arvoa tai virhettä kompositiologisesti. On kuitenkin korkea aika tarkastella monadeja.
Funktoreista Monadeihin
oletan, että ymmärrät miten funktorit toimivat ja miksi ne ovat hyödyllinen abstraktio. Mutta toiminnanharjoittajat eivät ole niin yleismaailmallisia kuin voisi olettaa. Mitä tapahtuu, jos muunnos funktio (yksi läpäissyt argumenttina kartta()) palauttaa functor instance sijaan yksinkertainen arvo? Funktorikin on vain arvo, joten mitään pahaa ei tapahdu. Palautetut tavarat sijoitetaan takaisin functoriin, jotta kaikki käyttäytyvät johdonmukaisesti. Kuvittele kuitenkin, että sinulla on tämä kätevä menetelmä merkkijonojen jäsentämiseen:
FOptional<Integer> tryParse(String s) { try { final int i = Integer.parseInt(s); return FOptional.of(i); } catch (NumberFormatException e) { return FOptional.empty(); }}
poikkeuksia ovat tyyppijärjestelmää ja toiminnallista puhtautta heikentävät sivuvaikutukset. Puhtaissa funktionaalisissa kielissä poikkeuksille ei ole sijaa. Emmehän ole koskaan kuulleet poikkeusten heittämisestä matematiikan tunneilla? Virheet ja laittomat ehdot esitetään nimenomaisesti käyttämällä arvoja ja kääreitä. Esimerkiksi tryParse () ottaa narun, mutta ei vain palauta intiä tai äänettömästi heitä poikkeusta runtaessa. Me nimenomaan kertoa, kautta Tyyppi järjestelmä, että tryParse () voi epäonnistua, ei ole mitään poikkeuksellista tai virheellistä ottaa epämuodostunut merkkijono. Tätä puolivirhettä edustaa valinnainen tulos. Mielenkiintoista Java on tarkistanut poikkeuksia, ne, jotka on ilmoitettava ja käsiteltävä, joten jossain mielessä, Java on puhtaampi siltä osin, se ei piilota sivuvaikutuksia. Mutta hyvässä tai pahassa tarkastetut poikkeukset ovat usein lannistettuja Jaavalla, joten palataan takaisin tryParse(). Tryparse kannattaa säveltää jo valmiiksi Foptionaaliseen käärittyyn naruun:
FOptional<String> str = FOptional.of("42");FOptional<FOptional<Integer>> num = str.map(this::tryParse);
sen ei pitäisi tulla yllätyksenä. Jos tryParse()
palauttaisi int
saatFOptional<Integer> num
, mutta koska map()
funktio palauttaa FOptional<Integer>
itse, se kietoutuu kahdesti hankalaksi FOptional<FOptional<Integer>>
. Katsokaa tarkkaan tyyppejä, teidän täytyy ymmärtää, miksi meillä on tämä kaksinkertainen kääre täällä. Sen lisäksi, että näyttää kauhealta, ottaa functor functor rauniot koostumus ja sujuva ketjuttaminen:
FOptional<Integer> num1 = //...FOptional<FOptional<Integer>> num2 = //...FOptional<Date> date1 = num1.map(t -> new Date(t));//doesn't compile!FOptional<Date> date2 = num2.map(t -> new Date(t));
tässä yritetään kartoittaa FOptional
sisältö kääntämällä int
muotoon +Date+. Kun funktio on int -> Date
, voimme helposti muuttaa sen Functor<Integer>
: stä Functor<Date>
: ksi, tiedämme miten se toimii. num2
tapauksessa tilanne kuitenkin mutkistuu. Se, mitä num2.map()
saa syötteenä, ei ole enää int
vaan FOoption<Integer>
ja ilmeisestijava.util.Date
ei ole tällaista konstruktiota. Rikoimme functorin paketoimalla sen. Kuitenkin funktio, joka palauttaa funktion yksinkertaisen arvon sijaan, on niin yleinen (kutentryParse()
), että emme voi yksinkertaisesti sivuuttaa tällaista vaatimusta. Yksi lähestymistapa on ottaa käyttöön erityinen parametriton join()
– menetelmä, joka ”litistää” sisäkkäisiä funktioita:
FOptional<Integer> num3 = num2.join()
se toimii, mutta koska tämä kuvio on niin yleinen, otettiin käyttöön erityinen flatMap()
– niminen menetelmä. flatMap()
on hyvin samankaltainen kuin map
, mutta odottaa argumenttina saadun funktion palauttavan funktion-tai monadin olevan tarkka:
interface Monad<T,M extends Monad<?,?>> extends Functor<T,M> { M flatMap(Function<T,M> f);}
päädyimme yksinkertaisesti siihen, että flatMap
on vain syntaktista sokeria paremman koostumuksen mahdollistamiseksi. MuttaflatMap
menetelmä (jota usein kutsutaan nimellä bind
tai >>=
haskellilta) tekee kaiken eron, koska se mahdollistaa monimutkaisten muunnosten säveltämisen puhtaalla, funktionaalisella tyylillä. Jos FOptional
oli monadin ilmentymä, jäsennys yhtäkkiä toimii odotetusti:
FOptional<String> num = FOptional.of("42");FOptional<Integer> answer = num.flatMap(this::tryParse);
monadien ei tarvitse toteuttaa map
, se voidaan toteuttaa flatMap()
päälle helposti. Itse asiassa flatMap
on oleellinen toimija, joka mahdollistaa kokonaan uuden muunnosuniversumin. Ilmeisesti aivan kuten funktoreilla, syntaktinen mukautuminen ei riitä kutsumaan jotain A-luokan monadia, flatMap()
operaattorin on noudatettava monadin lakeja, mutta ne ovat melko intuitiivisia kuten flatMap()
assosiatiivisuus ja identiteetti. Jälkimmäinen edellyttää, että m(x).flatMap(f)
on sama kuinf(x)
mille tahansa monadille, jonka arvo on x
, ja mille tahansa funktiolle f
. Emme aio sukeltaa liian syvälle monad-teoriaan, vaan keskitytään käytännön seurauksiin. Monadit loistavat, kun niiden sisäinen rakenne ei ole triviaali, esimerkiksi Promise
monadi, jolla on tulevaisuudessa arvo. Voitko arvata tyyppijärjestelmästä, miten Promise
käyttäytyy seuraavassa ohjelmassa? Ensinnäkin, kaikki menetelmät, jotka voivat mahdollisesti kestää jonkin aikaa palauttaa Promise
:
import java.time.DayOfWeek;Promise<Customer> loadCustomer(int id) { //...}Promise<Basket> readBasket(Customer customer) { //...}Promise<BigDecimal> calculateDiscount(Basket basket, DayOfWeek dow) { //...}
voimme nyt säveltää nämä toiminnot ikään kuin ne kaikki estäisivät käyttämällä monadisia operaattoreita:
Promise<BigDecimal> discount = loadCustomer(42) .flatMap(this::readBasket) .flatMap(b -> calculateDiscount(b, DayOfWeek.FRIDAY));
tästä tulee mielenkiintoista. flatMap()
on säilytettävä monadinen tyyppi, joten kaikki välikappaleet ovat Promise
s. Kyse ei ole vain tyyppien pitämisestä järjestyksessä – edeltävä ohjelma on yhtäkkiä täysin asynkroninen! loadCustomer()
palauttaa Promise
, joten se ei blokkaa. readBasket()
ottaa mitä tahansa Promise
on (tulee olemaan) ja soveltaa funktiota, joka palauttaa toisen Promise
ja niin edelleen. Periaatteessa rakensimme asynkronisen laskuputken, jossa yhden vaiheen suorittaminen taustalla laukaisee automaattisesti seuraavan vaiheen.
tutkimalla flatMap ()
on hyvin yleistä, että on kaksi monadia ja niiden yhdistämä arvo. Sekä toiminnanharjoittajat että monadit eivät kuitenkaan salli suoraa pääsyä sisuksiinsa, mikä olisi epäpuhdasta. Sen sijaan, meidän on huolellisesti soveltaa transformaatio pakenematta monad. Kuvittele, että sinulla on kaksi monadia ja haluat yhdistää ne:
import java.time.LocalDate;import java.time.Month;Monad<Month> month = //...Monad<Integer> dayOfMonth = //...Monad<LocalDate> date = month.flatMap((Month m) -> dayOfMonth .map((int d) -> LocalDate.of(2016, m, d)));
tutkikaa edeltävää valekoodia. En käytä mitään todellista monad-toteutusta, kuten Promise
tai List
, ydinkäsitteen korostamiseen. Meillä on kaksi itsenäistä monadia, joista toinen on tyyppiä Month
ja toinen tyyppiä Integer
. Jotta niistä voidaan rakentaa LocalDate
, on rakennettava sisäkkäinen muunnos, jolla on pääsy molempien monadien sisuksiin. Käy läpi tyypit, erityisesti varmistaen, että ymmärrät miksi käytämme flatMap
yhdessä paikassa jamap()
toisessa. Mieti, miten rakentaisit tämän koodin, jos sinulla olisi myös kolmas Monad<Year>
. Tämä kahden argumentin funktion (m
ja d
meidän tapauksessamme) soveltamistapa on niin yleinen, että Haskellissa on olemassa erityinen auttajafunktio liftM2
, joka tekee juuri tämän muunnoksen, joka toteutetaan map
ja flatMap
päälle. Java pseudo-syntaksi se näyttäisi hieman tältä:
Monad<R> liftM2(Monad<T1> t1, Monad<T2> t2, BiFunction<T1, T2, R> fun) { return t1.flatMap((T1 tv1) -> t2.map((T2 tv2) -> fun.apply(tv1, tv2)) );}
sinun ei tarvitse toteuttaa tätä menetelmää jokaiselle monadille, flatMap()
riittää, lisäksi se toimii johdonmukaisesti kaikille monadeille. liftM2
on erittäin hyödyllinen, kun miettii, miten sitä voi käyttää erilaisten monadien kanssa. Esimerkiksi listM2(list1, list2, function)
pätee function
jokaiselle mahdolliselle kappaleparille list1
ja list2
(Karteesinen tuote). Toisaalta, optionals se soveltaa funktio vain, kun molemmat optionals ovat ei-tyhjä. Vielä parempi, jos funktio Promise
monad suoritetaan asynkronisesti, kun molemmat Promise
s on suoritettu. Tämä tarkoittaa, että keksimme juuri yksinkertaisen synkronointimekanismin (join()
haarukkaliitosalgoritmeissa), jossa on kaksi asynkronista vaihetta.
toinen hyödyllinen toimija, jonka voimme helposti rakentaa flatMap()
päälle, on filter(Predicate<T>)
, joka ottaa kaiken monadin sisällä olevan ja hävittää sen kokonaan, jos se ei täytä tiettyä predikaattia. Tavallaan se on samanlainen kuin map
, mutta 1-to-1-kartoituksen sijaan meillä on 1-to-0-tai-1. Jälleen filter()
on sama semantiikka jokaiselle monadille, mutta varsin hämmästyttävä toiminnallisuus riippuen siitä, mitä monadia oikeasti käytämme. On selvää, että se mahdollistaa tiettyjen elementtien suodattamisen luettelosta.:
FList<Customer> vips = customers.filter(c -> c.totalOrders > 1_000);
, mutta se toimii yhtä hyvin esim. Tällöin voimme muuttaa ei-tyhjän valinnaisen tyhjäksi, jos valinnaisen sisältö ei täytä joitakin kriteerejä. Tyhjät vaihtoehdot on jätetty koskemattomiksi.
monadien luettelosta
toinen hyödyllinen operaattori, joka on lähtöisin flatmapista (), on sekvenssi(). Voit helposti arvata, mitä se tekee yksinkertaisesti katsomalla tyyppi allekirjoitus:
Monad<Iterable<T>> sequence(Iterable<Monad<T>> monads)
usein meillä on joukko samantyyppisiä monadeja ja haluamme yhden monadin, joka on sen tyyppinen lista. Tämä saattaa kuulostaa sinusta abstraktilta, mutta se on vaikuttavan hyödyllinen. Kuvitelkaa, että halusitte ladata muutaman asiakkaan tietokannasta samanaikaisesti ID: llä, joten käytitte loadCustomer(id)
– menetelmää useita kertoja eri tunnuksille, jolloin jokainen kutsu palasi Promise<Customer>
. Nyt sinulla on lista Promise
s, mutta oikeasti haluat listan asiakkaista, jotka esimerkiksi näytetään verkkoselaimessa. sequence()
(rxjavassa sequence()
kutsutaan concat()
tai merge()
, käyttötapauksesta riippuen) operaattori on rakennettu juuri tätä varten:
FList<Promise<Customer>> custPromises = FList .of(1, 2, 3) .map(database::loadCustomer);Promise<FList<Customer>> customers = custPromises.sequence();customers.map((FList<Customer> c) -> ...);
jolla on FList<Integer>
edustamassa asiakastunnuksia me map
sen yli (huomaatko miten auttaa, että FList
on funktori?) soittamalla database.loadCustomer(id)
jokaiselle ID: lle. Tästä seuraa varsin epämukava lista Promise
s. sequence()
pelastaa päivän, mutta tämäkään ei ole pelkkää syntaktista sokeria. Edeltävä koodi on täysin Estoton. Erilaisille monadeille sequence()
on edelleen järkeä, mutta eri laskennallisessa kontekstissa. Esimerkiksi se voi muuttua FList<FOptional<T>>
muotoon FOptional<FList<T>>
. Ja muuten, voit toteuttaasequence()
(aivan kuten map()
) päälle flatMap()
.
tämä on vain jäävuoren huippu, kun puhutaan flatMap()
: n ja yleisesti monadien käyttökelpoisuudesta. Vaikka monadit ovat peräisin melko epämääräisestä kategoriateoriasta, ne osoittautuivat erittäin hyödyllisiksi abstraktioiksi jopa olio-orientoituneissa ohjelmointikielissä, kuten Javassa. Monadeja palauttavien toimintojen säveltäminen on niin yleisesti hyödyllistä, että kymmenet toisiinsa liittymättömät luokat seuraavat monadista käyttäytymistä.
lisäksi kun monadin sisään kapseloi dataa, sitä on usein vaikea saada yksiselitteisesti ulos. Tällainen operaatio ei kuulu monadin käyttäytymiseen ja johtaa usein ei-idiomaattiseen koodiin. Esimerkiksi Promise.get()
on Promise<T>
voi teknisesti palauttaa T
, mutta vain estämällä, kun taas kaikki flatMap()
perustuvat operaattorit ovat estottomia. Toinen esimerkki on FOptional.get()
, mutta se voi epäonnistua, koska FOptional
voi olla tyhjä. Jopa FList.get(idx)
että kurkistaa tietyn elementin luettelosta kuulostaa kiusalliselta, koska for
silmukoita voi korvata map()
melko usein.
toivottavasti nyt ymmärrät, miksi monadit ovat nykyään niin suosittuja. Jopa oliokeskeisessä (- ish) kielessä, kuten Javassa, ne ovat varsin hyödyllinen abstraktio.