ez a cikk eredetileg a reaktív programozás RxJava könyvünk függeléke volt. A monádok bevezetése azonban, bár nagyon sok köze volt a reaktív programozáshoz, nem igazán illett ehhez. Ezért úgy döntöttem, hogy kiveszem, és ezt külön blogbejegyzésként közzéteszem. Tisztában vagyok azzal, hogy “a monádok félig helyes és félig teljes magyarázata” az új “Hello, world” a programozási blogokon. A cikk azonban a functorokat és a monádokat a Java adatstruktúrák és könyvtárak sajátos szögéből vizsgálja. Ezért gondoltam, hogy érdemes megosztani.
az RxJava-t olyan alapvető fogalmak alapján tervezték és építették, mint a functors, a monoids és a monads. Annak ellenére, hogy az Rx-et eredetileg imperatív C# nyelvre modellezték, és az RxJava-ról tanulunk, hasonlóan imperatív nyelven dolgozunk, a könyvtár gyökerei a funkcionális programozásban vannak. Nem kell meglepődnie, miután rájött, milyen kompakt az RxJava API. Van elég sok csak egy maroknyi alapvető osztályok, jellemzően változhatatlan, és minden áll a többnyire tiszta funkciók.
a funkcionális programozás (vagy funkcionális stílus) közelmúltbeli növekedésével, amelyet leggyakrabban olyan modern nyelvekben fejeznek ki, mint a Scala vagy a Clojure, a monádok széles körben megvitatott témává váltak. Sok folklór van körülöttük:
a monád egy monoid az endofunkciók kategóriájában, mi a probléma?
James Iry
a monád átka az, hogy ha egyszer megkapod a megvilágosodást, ha megérted – “Ó, ez az, ami van” -, elveszíted azt a képességet, hogy bárkinek elmagyarázd.
Douglas Crockford
a programozók túlnyomó többsége, különösen azok, akik nem rendelkeznek funkcionális programozási háttérrel, hajlamosak azt hinni, hogy a monádok valamilyen misztikus számítástechnikai koncepció, annyira elméleti, hogy nem tudnak segíteni programozási karrierjükben. Ez a negatív perspektíva több tucat cikknek és blogbejegyzésnek tulajdonítható, amelyek túl elvontak vagy túl szűkek. De kiderül, hogy a monádok körülöttünk vannak, még egy szabványos Java könyvtárban is, különösen a Java Development Kit (JDK) 8 óta (erről később). Ami abszolút briliáns, az az, hogy amint először megértitek a monádokat, hirtelen több egymástól független osztály és absztrakció válik ismertté, amelyek teljesen más célokat szolgálnak.
a monádok különféle, látszólag független fogalmakat általánosítanak, így a monád újabb inkarnációjának megtanulása nagyon kevés időt vesz igénybe. Például nem kell megtanulnia, hogy a CompletableFuture hogyan működik a Java 8-ban – ha rájössz, hogy ez egy monád, pontosan tudod, hogyan működik, és mit várhatsz a szemantikájától. És akkor hallani RxJava ami úgy hangzik, annyira más, hanem azért, mert megfigyelhető egy monád, nincs sok hozzá. Számos más példa van a monádokra, amelyekkel már találkoztatok anélkül, hogy tudtátok volna. Ezért ez a szakasz hasznos frissítő lesz, még akkor is, ha nem használja ténylegesen az RxJava-t.
Functors
mielőtt elmagyaráznánk, mi a monád, vizsgáljuk meg a functor nevű egyszerűbb konstrukciót . A functor egy gépelt adatszerkezet, amely magában foglal bizonyos érték (ek) et. Szintaktikai szempontból a functor egy konténer a következő API-val:
import java.util.function.Function;interface Functor<T> { <R> Functor<R> map(Function<T, R> f);}
de a puszta szintaxis nem elegendő ahhoz, hogy megértsük, mi a functor. Az egyetlen művelet, amely functor nyújt map (), hogy vesz egy függvény f. Ez a funkció megkapja, ami egy dobozban, átalakítja, és becsomagolja az eredményt, mint-van egy második functor. Kérjük, olvassa el figyelmesen. A Functor< T> mindig megváltoztathatatlan tároló, így a map soha nem változtatja meg az eredeti objektumot, amelyen végrehajtották. Ehelyett visszaadja az eredményt(vagy eredményeket – légy türelmes) egy vadonatúj functorba csomagolva, esetleg különböző típusú R. ezenkívül a functoroknak nem szabad semmilyen műveletet végrehajtaniuk, ha identitásfüggvényt alkalmaznak, azaz térkép (x -> x). Egy ilyen mintának mindig ugyanazt a functort vagy egyenlő példányt kell visszaadnia.
gyakran Functor< T> összehasonlítjuk egy doboz gazdaság példánya T, ahol az egyetlen módja kölcsönhatásban ez az érték átalakításával. Nincs azonban idiomatikus módszer a kibontásra vagy a functor elől való menekülésre. Az érték(ek) mindig a functor kontextusában maradnak. Miért hasznosak a functorok? Általánosítanak több közös idiómát, például gyűjteményeket, ígéreteket, opciókat stb. egyetlen, egységes API-val, amely mindegyikben működik. Hadd mutassak be néhány functort, hogy folyékonyabbá tegyem ezt az API-t:
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); }}
egy extra F típusú paraméterre volt szükség az identitás összeállításához. Amit az előző példában látott, az a legegyszerűbb functor volt, amely csak értéket tartott. Minden, amit tehetünk, hogy az érték átalakítja belül map módszer, de nincs módja annak, hogy kivonat. Ez túlmutat a tiszta functor keretein. A functorral való interakció egyetlen módja a típusbiztos transzformációk szekvenciáinak alkalmazása:
Identity<String> idString = new Identity<>("abc");Identity<Integer> idInt = idString.map(String::length);
vagy folyékonyan, akárcsak a függvények írása:
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);
ebből a szempontból a functor feletti leképezés nem sokban különbözik a láncolt függvények egyszerű meghívásától:
byte bytes = customer .getAddress() .street() .substring(0, 3) .toLowerCase() .getBytes();
miért is bajlódna egy ilyen bőbeszédű csomagolással, amely nemcsak nem nyújt hozzáadott értéket, de nem is képes a tartalom visszakeresésére? Nos, kiderül, hogy számos más fogalmat modellezhet ezzel a nyers functor absztrakcióval. Például kezdve Java 8 Opcionális egy functor a map () módszerrel. Hajtsuk végre a semmiből:
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); }}
most érdekes lesz. Egy FOptional<T>
functor tartalmazhat értéket, de lehet, hogy üres. Ez egy típusbiztos kódolási mód null
. A FOptional
felépítésének két módja van: érték megadásával vagy empty()
példány létrehozásával. Mindkét esetben, csakúgy, mint a Identity
esetében, aFOptional
is megváltoztathatatlan, és csak belülről tudunk kölcsönhatásba lépni az értékkel. AFOptional
abban különbözik, hogy a f
transzformációs függvény nem alkalmazható semmilyen értékre, ha üres. Ez azt jelenti, hogy a functor nem feltétlenül foglalja magába pontosan egy T
típusú értéket. Ez ugyanolyan jól csomagolja tetszőleges számú értéket, mint a 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); }}
az API ugyanaz marad: veszel egy functor egy átalakulás – de a viselkedés sokkal más. Most átalakítást alkalmazunk a FList minden egyes elemére, deklaratív módon átalakítva az egész listát. Tehát, ha van egy listája az ügyfelekről, és szeretne egy listát az utcáikról, ez olyan egyszerű, mint:
import static java.util.Arrays.asList;FList<Customer> customers = new FList<>(asList(cust1, cust2));FList<String> streets = customers .map(Customer::getAddress) .map(Address::street);
ez már nem olyan egyszerű, mint azt customers.getAddress().street()
, akkor nem hivatkozhatgetAddress()
a gyűjtemény az ügyfelek, meg kell hívni getAddress()
minden egyes ügyfél, majd helyezze vissza a gyűjtemény. Egyébként Groovy ezt a mintát annyira általánosnak találta, hogy valójában szintaxiscukorral rendelkezik erre: customer*.getAddress()*.street()
. Ez a spread-dot néven ismert operátor valójában egy map
álruhában. Talán kíváncsi vagy, miért iterálom át list
manuálisan belül map
ahelyett, hogy Stream
s-t használnék a Java 8-ból:list.stream().map(f).collect(toList())
? Ismerősen cseng? Mi lenne, ha azt mondanám,java.util.stream.Stream<T>
a Java egy functor is? Mellesleg, egy monád is?
most látnod kell a functorok első előnyeit – elvonják a belső ábrázolást, és konzisztens, könnyen használható API-t biztosítanak a különböző adatstruktúrákon. Utolsó példaként hadd mutassam be a promise functor-t, hasonlóan a Future
– hez. Promise
“ígéri”, hogy egy érték egy nap elérhetővé válik. Még nincs ott, talán azért, mert valamilyen háttérszámítás született, vagy külső eseményre várunk. De valamikor a jövőben megjelenik. Az aPromise<T>
teljesítésének mechanikája nem érdekes, de a functor jellege:
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);
ismerősnek tűnik? Ez a lényeg! A functor megvalósítása túlmutat e cikk hatályán, sőt nem is fontos. Elég azt mondani, hogy nagyon közel vagyunk a CompletableFuture implementálásához a Java 8-ból, és majdnem felfedeztük az RxJava-tól megfigyelhető. De vissza a funkcionáriusokhoz. Promise< ügyfél> még nem rendelkezik az ügyfél értékével. Azt ígéri, hogy ilyen értéket képvisel a jövőben. De még mindig feltérképezhetjük az ilyen functort, ahogy a FOptional és a FList esetében is tettük – a szintaxis és a szemantika pontosan ugyanaz. A viselkedés azt követi, amit a functor képvisel. Ügyfél meghívása.térkép (ügyfél:: getAddress) hozamok ígéret<cím>, ami azt jelenti, térkép nem blokkoló. ügyfél.térkép () az ügyfél megígéri, hogy teljes. Ehelyett egy másik, más típusú ígéretet ad vissza. Amikor az upstream ígéret befejeződik, a downstream ígéret a map () – nek átadott függvényt alkalmazza, és átadja az eredményt a downstream-nek. Hirtelen a functor lehetővé teszi számunkra, hogy a csővezeték aszinkron számítások egy nem blokkoló módon. De ezt nem kell megértened vagy megtanulnod – mivel az ígéret egy functor, követnie kell a szintaxist és a törvényeket.
sok más nagyszerű példa van a functorokra, például az értéket vagy a hibát kompozíciós módon képviselik. De itt az ideje, hogy megnézzük a monádokat.
a Functoroktól a Monádokig
feltételezem, hogy megérti, hogyan működnek a functorokés miért hasznos absztrakció. De a functorok nem olyan univerzálisak, mint amire számítani lehet. Mi történik, ha a transzformációs függvény (amelyet argumentumként adtak át a map () – nek) functor példányt ad vissza, nem pedig egyszerű értéket? Nos, a functor is csak egy érték, tehát semmi rossz nem történik. Bármit is visszaküldtek, visszahelyezik egy functorba, így minden következetesen viselkedik. Képzelje el azonban, hogy van ez a praktikus módszer a húrok elemzésére:
FOptional<Integer> tryParse(String s) { try { final int i = Integer.parseInt(s); return FOptional.of(i); } catch (NumberFormatException e) { return FOptional.empty(); }}
kivételek azok a mellékhatások, amelyek aláássák a típusrendszert és a funkcionális tisztaságot. A tiszta funkcionális nyelvekben nincs helye kivételeknek. Végül, soha nem hallottunk a kivételek dobásáról a matematikai órák során, igaz? A hibákat és az illegális feltételeket kifejezetten az értékek és a csomagolások segítségével ábrázolják. Például tryParse () vesz egy String, de nem egyszerűen vissza egy int vagy csendben dobja kivétel futás közben. A típusrendszeren keresztül kifejezetten elmondjuk, hogy a tryParse () meghibásodhat, nincs semmi kivételes vagy hibás abban, hogy hibás karakterlánc van. Ezt a félhibát egy opcionális eredmény képviseli. Érdekes módon a Java ellenőrizte a kivételeket, azokat, amelyeket deklarálni és kezelni kell, így bizonyos értelemben a Java tisztább ebben a tekintetben, nem rejti el a mellékhatásokat. De a jobb vagy rosszabb ellenőrzött kivételek gyakran kedvét Java, így térjünk vissza a tryParse (). Úgy tűnik, hasznos, hogy össze tryParse a húr már csomagolva FOptional:
FOptional<String> str = FOptional.of("42");FOptional<FOptional<Integer>> num = str.map(this::tryParse);
ez nem lehet meglepetés. Ha az tryParse()
egy int
értéket ad vissza, akkor aFOptional<Integer> num
értéket kapja, de mivel a map()
függvény maga a FOptional<Integer>
értéket adja vissza, kétszer becsomagolódik kínos FOptional<FOptional<Integer>>
. Kérjük, alaposan nézze meg a típusokat, meg kell értenie, miért van itt ez a kettős burkolat. Amellett, hogy szörnyen néz ki, a functor-ban lévő functor romok összetétele és folyékony láncolása:
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));
itt megpróbáljuk feltérképezni a FOptional
tartalmát úgy, hogy a int
értéket +Dátum+értékre fordítjuk. A int -> Date
függvény könnyen átalakítható Functor<Integer>
– ról Functor<Date>
– ra, tudjuk, hogyan működik. De num2
esetén a helyzet bonyolulttá válik. Amit a num2.map()
bemenetként kap, az már nem int
, hanem FOoption<Integer>
, és nyilvánvalóanjava.util.Date
nem rendelkezik ilyen konstruktorral. Mi tört a functor dupla csomagolás azt. Azonban, amelynek függvény, amely visszaadja a functor helyett egyszerű érték annyira gyakori (minttryParse()
), hogy nem tudjuk egyszerűen figyelmen kívül hagyni az ilyen követelmény. Az egyik megközelítés egy speciális paraméter nélküli join()
módszer bevezetése, amely” ellapítja ” a beágyazott functorokat:
FOptional<Integer> num3 = num2.join()
működik, de mivel ez a minta annyira gyakori, a flatMap()
nevű speciális módszert vezették be. flatMap()
nagyon hasonlít map
de elvárja, hogy az argumentumként kapott függvény egy functor – vagy monádot adjon vissza, hogy pontos legyek:
interface Monad<T,M extends Monad<?,?>> extends Functor<T,M> { M flatMap(Function<T,M> f);}
egyszerűen arra a következtetésre jutottunk, hogy flatMap
csak szintaktikai cukor, amely lehetővé teszi a jobb összetételt. DeflatMap
módszer (gyakran hívják bind
vagy >>=
tól től Haskell) minden különbséget jelent, mivel lehetővé teszi a komplex transzformációk tiszta, funkcionális stílusban történő összeállítását. Ha FOptional
a monád egy példánya volt, az elemzés hirtelen a várt módon működik:
FOptional<String> num = FOptional.of("42");FOptional<Integer> answer = num.flatMap(this::tryParse);
monádok nem kell végrehajtani map
, akkor lehet végrehajtani a tetején flatMap()
könnyen. Valójában flatMap
az alapvető operátor, amely lehetővé teszi az átalakulások teljesen új univerzumát. Nyilvánvaló, hogy csakúgy, mint a functoroknál, a szintaktikai megfelelés nem elég ahhoz, hogy valamilyen A osztályú monádot hívjunk, a flatMap()
operátornak követnie kell a monád törvényeit, de ezek meglehetősen intuitívak, mint a flatMap()
asszociativitása és az identitás. Ez utóbbi megköveteli, hogy a m(x).flatMap(f)
ugyanaz, mint af(x)
minden olyan monád esetében, amely x
értéket és f
függvényt tartalmaz. Nem fogunk túl mélyen belemerülni a monád elméletbe, inkább a gyakorlati következményekre összpontosítsunk. A monádok akkor ragyognak, ha belső szerkezetük nem triviális, például Promise
monád, amely a jövőben értéket fog tartani. Meg tudja tippelni a típusrendszerből, hogyan fog viselkedni az Promise
a következő programban? Először is, minden olyan módszer, amely potenciálisan eltarthat egy ideig a visszatérés befejezéséhezPromise
:
import java.time.DayOfWeek;Promise<Customer> loadCustomer(int id) { //...}Promise<Basket> readBasket(Customer customer) { //...}Promise<BigDecimal> calculateDiscount(Basket basket, DayOfWeek dow) { //...}
most ezeket a függvényeket úgy állíthatjuk össze, mintha mind blokkolnák a monadikus operátorokat:
Promise<BigDecimal> discount = loadCustomer(42) .flatMap(this::readBasket) .flatMap(b -> calculateDiscount(b, DayOfWeek.FRIDAY));
ez érdekes lesz. A flatMap()
– nek meg kell őriznie a monadikus típust, ezért minden köztes objektum Promise
s. Nem csak a típusok rendben tartásáról van szó-az előző program hirtelen teljesen aszinkron! loadCustomer()
visszaadja a Promise
, így nem blokkolja. readBasket()
elveszi azt, ami az Promise
– nek van (lesz), és alkalmaz egy függvényt, ami egy másik Promise
– et ad vissza, és így tovább. Alapvetően egy aszinkron számítási folyamatot építettünk, ahol a háttérben egy lépés befejezése automatikusan elindítja a következő lépést.
a flatMap () feltárása
nagyon gyakori, hogy két monád van, és a hozzájuk tartozó értéket egyesítik. Azonban mind a functorok, mind a monádok nem engednek közvetlen hozzáférést a belső részükhöz, ami tisztátalan lenne. Ehelyett gondosan kell alkalmaznunk az átalakulást anélkül, hogy elmenekülnénk a monádból. Képzeld el, hogy két monádod van, és össze akarod őket kapcsolni:
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)));
kérjük, szánjon rá időt az előző pszeudo-kód tanulmányozására. Nem használok valódi monad implementációt, például Promise
vagy List
az alapkoncepció hangsúlyozására. Két független monádunk van, az egyik Month
, a másik Integer
típusú. Annak érdekében, hogy LocalDate
– ot építsünk belőlük, egy beágyazott transzformációt kell építenünk, amely hozzáférést biztosít mindkét monád belső részeihez. Dolgozza át a típusokat, különös tekintettel arra, hogy megértse, miért használjuk az egyik helyen az flatMap
– et, a másikban pedig amap()
– et. Gondoljon arra, hogyan strukturálná ezt a kódot, ha lenne egy harmadik Monad<Year>
is. Ez a minta alkalmazása a függvény két argumentum (m
és d
esetünkben) annyira gyakori, hogy a Haskell van egy speciális segítő függvény úgynevezett liftM2
, hogy nem pontosan ezt az átalakítást, végre tetején map
és flatMap
. A Java pszeudo-szintaxisban kissé így nézne ki:
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)) );}
nem kell ezt a módszert minden monádra végrehajtania, a flatMap()
elegendő, ráadásul következetesen működik minden monád esetében. liftM2
rendkívül hasznos, ha figyelembe vesszük, hogyan lehet használni a különböző monádok. Például a listM2(list1, list2, function)
a function
– et a list1
és a list2
(derékszögű termék) minden lehetséges elempárjára alkalmazza. Másrészt az opciókhoz csak akkor fog függvényt alkalmazni, ha mindkét opció nem üres. Még jobb, ha Promise
monád esetén egy függvény aszinkron módon kerül végrehajtásra, ha mindkét Promise
s befejeződött. Ez azt jelenti, hogy csak feltaláltunk egy egyszerű szinkronizálási mechanizmust (join()
fork-join algoritmusokban) két aszinkron lépésből.
egy másik hasznos operátor, amelyet könnyen felépíthetünk a flatMap()
tetejére, a filter(Predicate<T>)
, amely elveszi azt, ami egy monád belsejében van, és teljesen eldobja, ha nem felel meg bizonyos predikátumnak. Bizonyos értelemben hasonló a map
-hez, de az 1-től 1-ig leképezés helyett 1-től 0-ig vagy-1-ig van. Ismét filter()
ugyanaz a szemantika minden monád esetében, de egészen elképesztő funkcionalitás attól függően, hogy melyik monádot használjuk. Magától értetődően, lehetővé teszi bizonyos elemek kiszűrését a listából:
FList<Customer> vips = customers.filter(c -> c.totalOrders > 1_000);
de ugyanolyan jól működik, mint pl. Ebben az esetben a nem üres opcionálist üressé alakíthatjuk, ha az opcionális tartalma nem felel meg bizonyos kritériumoknak. Az üres opciók érintetlenek maradnak.
a monádok listájától a
lista Monádjáig egy másik hasznos operátor, amely a flatMap () – ből származik, a sequence(). Könnyen kitalálni, hogy mit csinál egyszerűen nézi típusú aláírás:
Monad<Iterable<T>> sequence(Iterable<Monad<T>> monads)
gyakran van egy csomó azonos típusú monádunk,és szeretnénk egy ilyen típusú monádot. Lehet, hogy ez elvontnak tűnik számodra, de lenyűgözően hasznos. Képzelje el, hogy egyszerre szeretne betölteni néhány ügyfelet az adatbázisból azonosítóval, így a loadCustomer(id)
módszert többször használta különböző azonosítókhoz, minden egyes meghívás Promise<Customer>
értéket adott vissza. Most van egy listája Promise
s, de amit igazán akar, az az ügyfelek listája, pl. hogy megjelenjen a webböngészőben. A sequence()
(a RxJava sequence()
hívják concat()
vagy merge()
, attól függően, használati eset) operátor épül csak erre:
FList<Promise<Customer>> custPromises = FList .of(1, 2, 3) .map(database::loadCustomer);Promise<FList<Customer>> customers = custPromises.sequence();customers.map((FList<Customer> c) -> ...);
miután egy FList<Integer>
képviselő ügyfél azonosítók mi map
rajta (látod, hogyan segít, hogy FList
egy functor?) hívja database.loadCustomer(id)
minden ID. Ez meglehetősen kényelmetlen listához vezet Promise
s. sequence()
megmenti a napot, de ez ismét nem csak szintaktikai cukor. Az előző kód teljesen nem blokkoló. A különböző monádok sequence()
még mindig van értelme, de más számítási kontextusban. Például a FList<FOptional<T>>
értéket FOptional<FList<T>>
értékre változtathatja. Egyébként asequence()
(csakúgy, mint a map()
) a flatMap()
tetején is végrehajtható.
ez csak a jéghegy csúcsa, amikor a flatMap()
és általában a monádok hasznosságáról van szó. Annak ellenére, hogy meglehetősen homályos kategóriaelméletből származik, a monádok rendkívül hasznos absztrakciónak bizonyultak még olyan objektumorientált programozási nyelvekben is, mint a Java. A monádokat visszaadó függvények összeállítása annyira általánosan hasznos, hogy tucatnyi független osztály követi a monád viselkedést.
sőt, ha egyszer beágyazza az adatokat egy monádba, gyakran nehéz kifejezetten kiszedni. Az ilyen művelet nem része a monád viselkedésének, és gyakran nem idiomatikus kódhoz vezet. Például Promise.get()
tovább Promise<T>
technikailag visszatérhet T
, de csak blokkolással, míg a flatMap()
-en alapuló összes operátor nem blokkoló. Egy másik példa a FOptional.get()
, de ez sikertelen lehet, mert a FOptional
üres lehet. Még a FList.get(idx)
is, amely egy adott elemet kikukucskál egy listából, kínosan hangzik, mert a for
hurkokat elég gyakran cserélheti map()
– re.
remélem, most már érted, miért olyan népszerűek manapság a monádok. Még egy objektum-orientált(-ish) nyelv, mint a Java, ezek elég hasznos absztrakció.