Programowanie funkcyjne w czystej Javie: przykłady Functor i Monad

ten artykuł był początkowo dodatkiem do naszej książki Reactive Programming with RxJava. Jednak wprowadzenie do monad, choć bardzo związane z programowaniem reaktywnym, nie pasowało zbyt dobrze. Postanowiłem więc go wyjąć i opublikować osobno jako post na blogu. Zdaję sobie sprawę, że „moje własne, w połowie poprawne i w połowie kompletne wyjaśnienie monad” jest nowym „Hello, world” na blogach programistycznych. Jednak artykuł przygląda się funktorom i monadom z określonego punktu widzenia Java data structures and libraries. Dlatego pomyślałem, że warto się tym podzielić.

RxJava został zaprojektowany i zbudowany w oparciu o bardzo podstawowe pojęcia, takie jak funktory, monoidy i monady. Mimo że Rx był początkowo wzorowany na imperatywnym języku C# i uczymy się o RxJava, pracując nad podobnie imperatywnym językiem, Biblioteka ma swoje korzenie w programowaniu funkcyjnym. Nie powinieneś być zaskoczony, gdy zdasz sobie sprawę, jak kompaktowe jest API RxJava. Istnieje prawie tylko garść podstawowych klas, zazwyczaj niezmiennych, a wszystko składa się przy użyciu głównie czystych funkcji.

wraz z rozwojem programowania funkcyjnego (lub stylu funkcjonalnego), najczęściej wyrażanego we współczesnych językach, takich jak Scala lub Clojure, monady stały się szeroko dyskutowanym tematem. Wokół nich jest dużo Folkloru:

monada to monoid w kategorii endofunktorów, w czym problem?

James Iry

przekleństwo monady polega na tym, że kiedy dostajesz objawienie, kiedy rozumiesz – „o to właśnie chodzi” – tracisz zdolność do wyjaśnienia tego komukolwiek.

Douglas Crockford

zdecydowana większość programistów, zwłaszcza tych, którzy nie mają zaplecza programistycznego, uważa, że monady są tajemną koncepcją informatyczną, tak teoretyczną, że nie może to pomóc w ich karierze programistycznej. Tę negatywną perspektywę można przypisać dziesiątkom artykułów i postów na blogu, które są zbyt abstrakcyjne lub zbyt wąskie. Ale okazuje się, że monady są wszędzie wokół nas, nawet w standardowej bibliotece Java, zwłaszcza od Java Development Kit (JDK) 8 (więcej o tym później). Absolutnie genialne jest to, że gdy po raz pierwszy zrozumiesz monady, nagle poznasz kilka niepowiązanych klas i abstrakcji, służących zupełnie innym celom.

monady uogólniają różne pozornie niezależne pojęcia tak, że poznanie kolejnego wcielenia monady zajmuje bardzo mało czasu. Na przykład, nie musisz się uczyć, jak CompletableFuture działa w Javie 8 – kiedy uświadomisz sobie, że jest to monada, dokładnie wiesz, jak działa i czego możesz oczekiwać od jej semantyki. A potem słyszycie o Rxjavie, która brzmi zupełnie inaczej, ale ponieważ Observable to monada, nie ma wiele do dodania. Istnieje wiele innych przykładów monad, które już natknąłeś się nie wiedząc o tym. Dlatego ta sekcja będzie użytecznym odświeżaczem, nawet jeśli nie używasz RxJava.

Functory

zanim wyjaśnimy czym jest monada, przyjrzyjmy się prostszej konstrukcji zwanej funktorem . Funktor jest typowaną strukturą danych, która zawiera pewne wartości. Z punktu widzenia składniowego funktor jest kontenerem z następującym API:

import java.util.function.Function;interface Functor<T> { <R> Functor<R> map(Function<T, R> f);}

ale sama składnia nie wystarczy, aby zrozumieć, czym jest funktor. Jedyną operacją, którą dostarcza functor, jest map (), która przyjmuje funkcję f. ta funkcja odbiera to, co znajduje się w pudełku, przekształca je i zawija wynik jako-is w drugi functor. Przeczytaj to uważnie. Functor<t> jest zawsze niezmiennym kontenerem, więc map nigdy nie mutuje oryginalnego obiektu, na którym został wykonany. Zamiast tego zwraca wynik(lub wyniki – bądź cierpliwy) zawinięty w zupełnie nowy funktor, być może innego typu R. dodatkowo funktory nie powinny wykonywać żadnych działań, gdy zastosowana jest funkcja tożsamości, czyli map (x -> x). Taki wzorzec powinien zawsze zwracać ten sam funktor lub taką samą instancję.

często funktor < T > jest porównywany do instancji t, gdzie jedynym sposobem interakcji z tą wartością jest jej przekształcenie. Nie ma jednak idiomatycznego sposobu na rozpakowanie lub ucieczkę od funktora. Wartości zawsze pozostają w kontekście funktora. Dlaczego funktory są przydatne? Uogólniają one wiele popularnych idiomów, takich jak zbiory, obietnice, opcje itp. z jednym, jednolitym API, które działa we wszystkich z nich. Pozwól, że przedstawię Ci kilka funkcji, które sprawią, że będziesz bardziej płynny z tym API:

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); }}

dodatkowy parametr typu F był wymagany do skompilowania tożsamości. To, co zobaczyłeś w poprzednim przykładzie, to najprostszy funktor trzymający tylko wartość. Wszystko, co możesz zrobić z tą wartością, to przekształcić ją wewnątrz metody map, ale nie ma sposobu, aby ją wyodrębnić. Jest to rozważane poza zakresem czystego funktora. Jedynym sposobem na interakcję z functorem jest zastosowanie sekwencji transformacji bezpiecznych dla typów:

Identity<String> idString = new Identity<>("abc");Identity<Integer> idInt = idString.map(String::length);

lub płynnie, tak jak komponujesz funkcje:

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);

z tej perspektywy mapowanie nad funktorem nie różni się zbytnio od wywoływania funkcji łańcuchowych:

byte bytes = customer .getAddress() .street() .substring(0, 3) .toLowerCase() .getBytes();

po co w ogóle zawracać sobie głowę takim gadatliwym opakowaniem, które nie tylko nie zapewnia żadnej wartości dodanej, ale także nie jest w stanie wydobyć zawartości z powrotem? Okazuje się, że można modelować kilka innych pojęć używając tej surowej abstrakcji funkcyjnej. Na przykład począwszy od Java 8 opcjonalny jest funktor z metodą map (). Zaimplementujmy go od podstaw:

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); }}

teraz staje się interesujące. Funktor FOptional<T> może posiadać wartość, ale równie dobrze może być pusty. Jest to Bezpieczny dla typu sposób kodowania null. Istnieją dwa sposoby konstruowania FOptional – przez podanie wartości lub utworzenie instancji empty(). W obu przypadkach, podobnie jak w przypadku Identity,FOptional jest niezmienna i możemy oddziaływać tylko z wartością od wewnątrz. Tym, co różni sięFOptional, jest to, że funkcja transformacji f nie może być zastosowana do żadnej wartości, jeśli jest pusta. Oznacza to, że functor może niekoniecznie zawierać dokładnie jedną wartość typu T. Równie dobrze może zapakować dowolną liczbę wartości, tak jak 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 pozostaje takie samo: bierzesz funktora w transformacji – ale zachowanie jest znacznie inne. Teraz stosujemy transformację dla każdego elementu na Fliście, deklaratywnie przekształcając całą listę. Więc jeśli masz listę klientów i chcesz listę ich ulic, to jest tak proste, jak:

import static java.util.Arrays.asList;FList<Customer> customers = new FList<>(asList(cust1, cust2));FList<String> streets = customers .map(Customer::getAddress) .map(Address::street);

nie jest to już tak proste, jak powiedzenie customers.getAddress().street(), nie możesz wywołaćgetAddress() na kolekcji klientów, musisz wywołać getAddress() na każdym indywidualnym kliencie, a następnie umieścić go z powrotem w kolekcji. Przy okazji, Groovy znalazł ten wzór tak powszechny, że faktycznie ma do tego cukier składniowy: customer*.getAddress()*.street(). Operator ten, znany jako spread-dot, jest w rzeczywistości map w przebraniu. Może zastanawiasz się, dlaczego ręcznie iteruję przez list wewnątrz mapzamiast używać StreamS Z Java 8:list.stream().map(f).collect(toList())? Coś ci to mówi? A gdybym ci powiedział, żejava.util.stream.Stream<T> w Javie też jest funktorem? A przy okazji, również monada?
teraz powinieneś zobaczyć pierwsze zalety funktorów-abstraktują one wewnętrzną reprezentację i zapewniają spójne, łatwe w użyciu API nad różnymi strukturami danych. Jako ostatni przykład pozwolę sobie przedstawić funkcję promise, podobną do Future. Promise „obiecuje”, że pewnego dnia wartość stanie się dostępna. Jeszcze go nie ma, może dlatego, że pojawiły się jakieś obliczenia w tle lub czekamy na zdarzenie zewnętrzne. Ale pojawi się kiedyś w przyszłości. Mechanika wypełniania Promise<T> nie jest ciekawa, ale charakter funkcji jest:

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);

wygląda znajomo? O to chodzi! Implementacja functora wykracza poza zakres tego artykułu i nie jest nawet ważna. Wystarczy powiedzieć, że jesteśmy bardzo blisko implementacji CompletableFuture z Javy 8 i prawie odkryliśmy Observable z RxJava. Ale wracając do funkcji. Promise< Klient > nie posiada jeszcze wartości klienta. Obiecuje mieć taką wartość w przyszłości. Ale nadal możemy odwzorować taki funktor, tak jak zrobiliśmy to z FOptional i FList – składnia i semantyka są dokładnie takie same. Zachowanie podąża za tym, co reprezentuje funktor. Wywoływanie klienta.map (Klient::getAddress) daje obietnicę < adres>, co oznacza, że map nie blokuje się. klient.map() obiecuje klientowi ukończyć. Zamiast tego zwraca inną obietnicę, innego rodzaju. Gdy upstream promise zakończy działanie, downstream promise zastosuje funkcję przekazaną do map () i przekaże wynik w dół. Nagle nasz funktor pozwala nam na przesyłanie asynchronicznych obliczeń w sposób nieblokujący. Ale nie musisz tego rozumieć ani się uczyć – ponieważ Promise jest funktorem, musi podążać za składnią i prawami.

istnieje wiele innych wspaniałych przykładów funktorów, na przykład reprezentujących wartość lub błąd w sposób kompozycyjny. Ale najwyższy czas spojrzeć na monady.

od funktorów do Monad

zakładam, że rozumiesz, jak działają funktory i dlaczego są użyteczną abstrakcją. Ale funkcjonory nie są tak uniwersalne, jak można by się spodziewać. Co się stanie, jeśli funkcja transformacji (ta przekazana jako argument do map()) zwróci instancję functora zamiast prostej wartości? Cóż, funktor jest tylko wartością, więc nic złego się nie dzieje. Cokolwiek zostało ZWRÓCONE, jest umieszczane z powrotem w funktorze, więc wszystko zachowuje się konsekwentnie. Jednak wyobraź sobie, że masz tę poręczną metodę parsowania ciągów:

FOptional<Integer> tryParse(String s) { try { final int i = Integer.parseInt(s); return FOptional.of(i); } catch (NumberFormatException e) { return FOptional.empty(); }}

wyjątki są skutkami ubocznymi, które podważają system typu i czystość funkcjonalną. W czystych językach funkcjonalnych nie ma miejsca na wyjątki. W końcu nigdy nie słyszeliśmy o rzucaniu WYJĄTKÓW na lekcjach matematyki, prawda? Błędy i nielegalne warunki są wyraźnie reprezentowane za pomocą wartości i opakowań. Na przykład tryParse() pobiera łańcuch znaków, ale nie zwraca po prostu int lub po cichu wyrzuca wyjątek w czasie wykonywania. Wyraźnie mówimy, poprzez system typów, że metoda tryParse () może zawieść, nie ma nic wyjątkowego lub błędnego w nieprawidłowym kształcie Łańcucha. Ta pół-porażka jest reprezentowana przez opcjonalny wynik. Co ciekawe, Java sprawdziła wyjątki, te, które muszą być zadeklarowane i obsługiwane, więc w pewnym sensie Java jest czystsza pod tym względem, nie ukrywa skutków ubocznych. Ale na lepsze lub gorsze sprawdzane wyjątki są często odradzane w Javie, więc wróćmy do tryParse (). Przydatne wydaje się komponowanie tryParse z ciągiem już zawiniętym w FOptional:

FOptional<String> str = FOptional.of("42");FOptional<FOptional<Integer>> num = str.map(this::tryParse);

to nie powinno być zaskoczeniem. Jeśli tryParse() zwróci int, otrzymaszFOptional<Integer> num, ale ponieważ map() funkcja zwraca samą FOptional<Integer>, zostanie dwukrotnie opakowana w niezręczną FOptional<FOptional<Integer>>. Przyjrzyj się uważnie typom, musisz zrozumieć, dlaczego mamy tutaj podwójną owijkę. Poza wyglądem okropnym, posiadanie funktora w funktorze niszczy kompozycję i płynne Wiązanie:

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));

tutaj staramy się odwzorować zawartość FOptional, zmieniając int w +Date+. Mając funkcję int -> Date możemy łatwo przekształcić się z Functor<Integer> na Functor<Date>, wiemy jak to działa. Ale w przypadku num2 sytuacja staje się skomplikowana. To, co num2.map()otrzymuje jako Wejście, nie jest już int, ale FOoption<Integer> i oczywiściejava.util.Date nie ma takiego konstruktora. Złamaliśmy naszego funkcjonariusza, zawijając go podwójnie. Jednak posiadanie funkcji, która zwraca functor zamiast prostej wartości jest tak powszechne (jaktryParse()), że nie możemy po prostu zignorować takiego wymogu. Jednym z podejść jest wprowadzenie specjalnej metody bez parametru join(), która „spłaszcza” zagnieżdżone funktory:

FOptional<Integer> num3 = num2.join()

działa, ale ponieważ ten wzorzec jest tak powszechny, wprowadzono specjalną metodę o nazwie flatMap(). flatMap() jest bardzo podobny do map, ale oczekuje, że funkcja otrzymana jako argument zwróci funktor-lub monadę, aby być precyzyjnym:

interface Monad<T,M extends Monad<?,?>> extends Functor<T,M> { M flatMap(Function<T,M> f);}

po prostu doszliśmy do wniosku, że flatMap jest tylko cukrem składniowym, aby umożliwić lepszą kompozycję. Jednak metodaflatMap (często nazywana bind lub >>= od Haskella) robi różnicę, ponieważ pozwala na komponowanie złożonych transformacji w czystym, funkcjonalnym stylu. Jeśli FOptional był instancją monad, parsowanie nagle działa zgodnie z oczekiwaniami:

FOptional<String> num = FOptional.of("42");FOptional<Integer> answer = num.flatMap(this::tryParse);

monady nie muszą implementować map, można je łatwo zaimplementować na flatMap(). W rzeczywistości flatMap jest niezbędnym operatorem, który umożliwia zupełnie nowy wszechświat transformacji. Oczywiście, podobnie jak w przypadku funktorów, zgodność składniowa nie wystarczy, aby wywołać jakąś monadę klasy A, operator flatMap() musi przestrzegać praw monady, ale są one dość intuicyjne, jak Asocjacja flatMap() i tożsamość. To ostatnie wymaga, aby m(x).flatMap(f) było takie samo jakf(x) dla każdej monady posiadającej wartość x i dowolnej funkcji f. Nie będziemy zanurkować zbyt głęboko w teorię monad, zamiast tego skupmy się na praktycznych implikacjach. Monady świecą, gdy ich wewnętrzna struktura nie jest trywialna, na przykład Promise monada, która będzie miała wartość w przyszłości. Czy możesz odgadnąć z systemu typów, jak Promise będzie się zachowywać w następującym programie? Po pierwsze, wszystkie metody, które potencjalnie mogą zająć trochę czasu, aby zakończyć zwracanie Promise:

import java.time.DayOfWeek;Promise<Customer> loadCustomer(int id) { //...}Promise<Basket> readBasket(Customer customer) { //...}Promise<BigDecimal> calculateDiscount(Basket basket, DayOfWeek dow) { //...}

możemy teraz komponować te funkcje tak, jakby wszystkie blokowały za pomocą operatorów monadycznych:

Promise<BigDecimal> discount = loadCustomer(42) .flatMap(this::readBasket) .flatMap(b -> calculateDiscount(b, DayOfWeek.FRIDAY));

to staje się interesujące. flatMap() musi zachować Typ monadyczny, dlatego wszystkie obiekty pośrednie są Promise s. Nie chodzi tylko o utrzymanie typów w porządku-poprzedzający program jest nagle w pełni asynchroniczny! loadCustomer() zwraca Promise, więc nie blokuje. readBasket() bierze cokolwiek Promise ma (będzie miał) i stosuje funkcję zwracającą inny Promise i tak dalej i tak dalej. Zasadniczo zbudowaliśmy asynchroniczny potok obliczeń, w którym ukończenie jednego kroku w tle automatycznie uruchamia następny krok.

Exploring flatMap()

jest bardzo często mieć dwie monady i łączenie wartości, które zawierają razem. Jednak zarówno funktory, jak i monady nie pozwalają na bezpośredni dostęp do ich wnętrza, co byłoby nieczyste. Zamiast tego musimy ostrożnie stosować transformację, nie uciekając przed monadą. Wyobraź sobie, że masz dwie monady i chcesz je połączyć:

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)));

proszę poświęcić trochę czasu na przestudiowanie poprzedniego pseudo-kodu. Nie używam żadnej prawdziwej implementacji monad, takiej jak Promise lub List, aby podkreślić podstawową koncepcję. Mamy dwie niezależne monady, jedną typu Monthi drugą typu Integer. Aby zbudować z nich LocalDate, musimy zbudować zagnieżdżoną transformację, która ma dostęp do wewnętrznych elementów obu monad. Pracuj nad typami, szczególnie upewniając się, że rozumiesz, dlaczego używamy flatMap w jednym miejscu imap() w drugim. Pomyśl, jak byś ustrukturyzował ten kod, gdybyś miał również trzecią Monad<Year>. Ten wzorzec stosowania funkcji dwóch argumentów (w naszym przypadkum i d) jest tak powszechny, że w Haskell istnieje specjalna funkcja pomocnicza o nazwie liftM2, która wykonuje dokładnie tę transformację, zaimplementowana na map i flatMap. W pseudo-składni Javy wyglądałoby to nieco tak:

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)) );}

nie musisz implementować tej metody dla każdej monady, flatMap() wystarczy, co więcej, działa ona konsekwentnie dla wszystkich monad. liftM2 jest niezwykle przydatny, gdy zastanowimy się, jak można go używać z różnymi monadami. Na przykład listM2(list1, list2, function) zastosuje function dla każdej możliwej pary elementów z list1 i list2 (iloczyn kartezjański). Z drugiej strony, dla opcji zastosuje funkcję tylko wtedy, gdy obie opcje nie są puste. Co więcej, dla monad Promise funkcja zostanie wykonana asynchronicznie, gdy oba Promises zostaną zakończone. Oznacza to, że właśnie wynaleźliśmy prosty mechanizm synchronizacji (join() w algorytmach fork-join) dwóch kroków asynchronicznych.

kolejnym użytecznym operatorem, który możemy łatwo zbudować na flatMap(), jest filter(Predicate<T>), który pobiera wszystko, co znajduje się wewnątrz monady i odrzuca ją całkowicie, jeśli nie spełnia określonego predykatu. W pewnym sensie jest to podobne do map, ale zamiast mapowania 1-do-1 mamy 1-do-0-LUB-1. Ponownie filter() ma tę samą semantykę dla każdej monady, ale całkiem niesamowitą funkcjonalność w zależności od tego, której monady faktycznie używamy. Oczywiście, umożliwia filtrowanie niektórych elementów z listy:

FList<Customer> vips = customers.filter(c -> c.totalOrders > 1_000);

ale działa równie dobrze np. dla opcji. W takim przypadku możemy przekształcić niepuste opcjonalne w puste, jeśli zawartość opcjonalnego nie spełnia pewnych kryteriów. Puste opcje pozostają nienaruszone.

z listy Monad do Monad listy

innym użytecznym operatorem pochodzącym z flatmap() jest sequence(). Możesz łatwo zgadnąć, co robi po prostu patrząc na podpis typu:

Monad<Iterable<T>> sequence(Iterable<Monad<T>> monads)

często mamy kilka monad tego samego typu i chcemy mieć jedną monadę listy tego typu. Może to zabrzmieć abstrakcyjnie, ale jest imponująco użyteczne. Wyobraź sobie, że chcesz załadować kilku klientów z bazy danych jednocześnie przez ID, więc użyłeś metody loadCustomer(id) kilka razy dla różnych identyfikatorów, każde wywołanie zwracało Promise<Customer>. Teraz masz listę Promise s, ale to, co naprawdę chcesz, to lista klientów, np. do wyświetlenia w przeglądarce internetowej. Operator sequence() (w RxJava sequence() nazywa się concat() lub merge(), w zależności od przypadku użycia) jest zbudowany właśnie do tego:

FList<Promise<Customer>> custPromises = FList .of(1, 2, 3) .map(database::loadCustomer);Promise<FList<Customer>> customers = custPromises.sequence();customers.map((FList<Customer> c) -> ...);

mając FList<Integer> reprezentując identyfikatory klientów, mamy map nad nim (widzisz, jak to pomaga, że FListjest funktorem?) wywołując database.loadCustomer(id) dla każdego ID. Prowadzi to do dość niewygodnej listy Promises. sequence() ratuje dzień, ale po raz kolejny nie jest to tylko cukier składniowy. Poprzedni kod jest w pełni nieblokujący. Dla różnych rodzajów monad sequence() nadal ma sens, ale w innym kontekście obliczeniowym. Na przykład może zmienić FList<FOptional<T>> na FOptional<FList<T>>. A przy okazji, możesz zaimplementowaćsequence()(tak jak map()) na górze flatMap().

to tylko wierzchołek góry lodowej, jeśli chodzi o użyteczność flatMap() i monad w ogóle. Mimo że monady wywodzą się z dość niejasnej teorii kategorii, okazały się niezwykle użyteczną abstrakcją nawet w obiektowych językach programowania, takich jak Java. Możliwość komponowania funkcji zwracających monady jest tak uniwersalnie pomocna, że dziesiątki niepowiązanych klas podążają za zachowaniem monadycznym.

co więcej, po zamknięciu danych w monadzie często trudno jest je wyraźnie wyjąć. Taka operacja nie jest częścią zachowania monad i często prowadzi do kodu nie-idiomatycznego. Na przykład Promise.get() na Promise<T> może technicznie zwrócić T, ale tylko przez blokowanie, podczas gdy wszystkie operatory oparte na flatMap() nie są blokowane. Innym przykładem jest FOptional.get(), ale może się to nie udać, ponieważ FOptional może być pusta. Nawet FList.get(idx), który zagląda do konkretnego elementu z listy, brzmi niezręcznie, ponieważ można dość często zastępować pętle for pętlami map().

mam nadzieję, że teraz rozumiesz, dlaczego monady są tak popularne w dzisiejszych czasach. Nawet w języku obiektowym (- owskim), takim jak Java, są one dość użyteczną abstrakcją.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.