Wraz z wejściem Javy 8 i jej funkcyjnych elementów otrzymaliśmy też możliwość pobawienia się monadami. Monada to jest takie „coś” pochodzące z teorii kategorii co spełnia pewne prawa (definicja przez spejsonizację). Nie chcę tu wnikać w szczegóły, bo zarówno wiki jak i różne artykuły po sieci rozsiane tłumaczą co i jak. W świecie Javy sprowadza się to do (uwaga to moje pisanie na czuja, może być nie dobrze)

  • Monada posiada generyczny konstruktor, który jako parametr przyjmuje pewną wartość
  • Można na monadzie wykonać funkcję F i wynik będzie taki sam jak w przypadku bezpośredniego zaaplikowania tej funkcji do wartości. Przy czym F zwróci wartość opakowaną w monadę.
  • Można na monadzie zaaplikować pewną funkcję F tak, że po jej zaaplikowaniu otrzymamy naszą oryginalna monadę.
  • Jeżeli mamy dwie funkcje F i G to aplikując je na monadzie otrzymamy taki sam rezultat jak byśmy zaaplikowali pierwszą na wartości i kolejną na monadzie

I teraz bardziej formalnie

W ogólności mówimy o trzech prawach monad, a sama monada poza konstruktorem ma jeszcze dwie metody:

  • unit(value) – metodę, która opakowuje wartość w monadę
  • bind(F(value)) – metodę, która otwiera monadę, dokonuje pewnej operacji F na wartości i następnie zwraca monadę z nową wartością.

zaś prawa można zdefiniować:

  • unit(a).bind(f) == f(a) – zaaplikowanie funkcji na monadzie bind(f), unit(a) tworzy monadę, jest równoznaczne z zaaplikowaniem na wartości f(a)
  • m.bind(unit) == m – czyli, że jest sobie pewien unit, który jest operacją-niezmiennikiem dla monady.
  • m.bind(f).bind(g) == m.bind(x -> f(x).bind(g)) – czyli możemy komponować funkcje i nie zmieni to wyniku.

W Javie 8 mamy kilka różnych monad. Jedną z nich jest Optional i choć niektórzy piszą, że jest ona walnięta to osobiście tak nie uważam. Problemem z Javą 8 jest to, że API stwarza wrażenie pisanego na szybko. Brakuje w nim wielu elementów, które zazwyczaj się pojawiają w kodzie i nie są trudne koncepcyjnie do zaimplementowania. W efekcie powstaje wiele różnych implementacji, które oczywiście nie są ze sobą kompatybilne w żaden rozsądny sposób co prowadzi do pisania kolejnych, tym razem adapterów.

Nie widzę zatem przeciwwskazań by nie dokładać kolejnej. Jest to oczywiście tylko ćwiczenie i jeżeli chcemy by nasz projekt miał ręce i nogi to warto podpiąć jako zależność bibliotekę Javaslang gdzie klasa Try jest już zaimplementowana.

Jeszcze o motywacji

Celem wynajdywania koła na nowi jest nie tylko stworzenie bardziej okrągłego koła, ale też możliwość samodzielnego prześledzenia całego procesu twórczego. Nie jest to zresztą nic nowego ponieważ w kilku już źródłach spotkałem się z takim podejściem. Przykładowo w kursie programowania funkcyjnego w skali Martina Oderkiego jak i w książce Runara Bjarnasona poruszającej ten sam temat ćwiczymy implementację podstawowych struktur jak listy czy drzewa.

Wpis z krótkiego zrobił się bezczelnie długi i zostanie podzielony na dwie części. W pierwszej zdefiniujemy sobie interfejs oraz go omówimy. W drugiej powstanie implementacja oraz zweryfikujemy prawdziwość praw w stosunku do naszej zabawki.

Monada Try

Zastanówmy się co chcemy modelować za pomocą tego rozwiązania. Chcemy w jakiś sposób obudować operację, która może zwrócić wyjątek. Zatem wiemy, że powinna ona mieć dwa stany. Jeden reprezentujący sukces, drugi porażkę. Tu należy zadać sobie pytanie dlaczego nie Either. Otóż Either reprezentuje wynik, który może być jednego typu z dwóch co odpowiada mniej więcej logicznej dysjunkcji, a mówiąc po ludzku bramce NAND 🙂
Oczywistą rzeczą jest, że musimy też zwrócić w jakiś sposób wartość operacji albo błąd, który ona zwróciła.
Mamy zatem już zestaw czterech metod, plus jakaś metoda tworząca/konstruktor. Jednak żadna z nich, poza naszym konstruktorem/metodą tworzącą nie zapewnia nam spełnienia praw. Potrzebujemy jeszcze jednej metody, którą w celu utrzymania konwencji nazywamy flatMap. Do tego przydała by się jeszcze map (robiąca praktycznie to samo, ale w trochę inny sposób) oraz możliwość powtórzenia operacji. Dodatkowo by dało się tego używać z Javą 8 dodamy możliwość zamiany w Optional

Zatem kod będzie wyglądał mniej więcej tak:

Listing 1. Interfejs Try

public abstract class Try<T> {

	protected Exception exception;

	protected T value;

	protected Callable<T> operation;

	private Try(){}

	public static <R> Try<R> of(Callable<R> c) {
	///....
	}

	public <U> Try<U> flatMap(final Function<? super T, Try<U>> mapper) {
	///....
	}

	public <U> Try<U> map(final Function<? super T, U> mapper){
	///....
	}

	public void onSuccess(Consumer<T> consumer) {
	///....
	}

	public void onFailure(Consumer<Exception> consumer) {
	///....
	}

	public Optional<T> toOptional() {
	///....
	}

	public abstract boolean isSuccess();

	public abstract boolean isFailure();

	public abstract T get();

	public abstract Try<T> retry();

	public abstract Exception getReason();
//...
}

Wait… mówiłem o interfejsie mamy klasę abstrakcyjną, co szanowny pan za przeproszeniem odpierdala, jeśli można zapytać? Bardziej doświadczeni programiści powinni rozpoznać o co chodzi, a ci mniej niech teraz uważają. Generalnie w Javie jeżeli chcemy ograniczyć ilość implementacji danego interfejsu to się nie da. Po prostu każdy może wziąć zaimplementować nasz interfejs. Można by teoretycznie użyć enuma, ale enum ma tą wadę, że będzie trzymać stan zatem nie nadaje się na kontener. W cywilizowanych językach funkcyjnych zazwyczaj mamy do czynienia z jakimś mechanizmem strażników albo innym pattern matchingiem. W Scali są sobie case classy i sealed traits i właśnie coś podobnego do nich chcę tu uzyskać.
Zwróćcie uwagę na konstruktor. Jest on prywatny, co oznacza, że nie będzie widoczny poza tą klasą. Zatem jedyną metodą na implementację klas dziedziczących po Try jest umieszczenie ich w tej klasie jako klas wewnętrznych. To jest zresztą sztuczka do omówienia w przyszłości. Popatrzmy na inne bebechy.

Najpierw mamy trzy pola reprezentujące wynik operacji, błąd oraz samą operację. To ostatnie pozwoli nam na powtórzenie operacji jak by co. Później mamy konstruktor i metodę tworzącą. Naszym założeniem jest, że operacja coś zwraca. Zatem nie ma wersji z Runnable, ale jak komuś potrzebna to zachęcam do samodzielnej implementacji.

Następnie mamy parę flatMap i map. Różnica pomiędzy tymi dwoma metodami jest widoczna w sygnaturze. Pierwsza z nich oczekuje jako parametru funkcji, która jawnie zamieni nam T na M<U> (notacja M<U> oznacza monada od U i jest często spotykanym uogólnieniem). Druga dokonuje niejawnej konwersji na M<U> co pozwala na użycie funkcji T→U. Uwaga! wywołanie map z funkcją T→M<U> wyprodukuje nam T→M<M<U>>, co będzie wrzodem na kodzie.

Kolejne dwie metody reprezentują tak naprawdę opakowanie dla if-a, który nie zawiera return. Kolejna to wspomniana zamiana na Optional. Kolejne pięć metod jest już oczywiste. Zwrócą nam one informacje o stanie, wartości, będzie albo wywołają raz jeszcze operację.

Część z tych metod jest abstrakcyjna, część nie. Ich dokładną implementację omówię w kolejnej części.

Podsumowanie

Na razie udało nam się zdefiniować co chcemy mieć w naszym kodzie. Nadaliśmy tym oczekiwaniom kształt w postaci klasy Try i określiliśmy jaka będzie jej implementacja. Kolejny krok to implementacja poszczególnych metod i przede wszystkim zbadanie czy nasza klasa spełnia założenia.