Jak używać monad w Javie?

Przemysław Sobierajski

Zaktualizowaliśmy ten tekst dla Ciebie!
Data aktualizacji: 12.12.2024
Autor aktualizacji: Tomasz Lenartowski

Java nie jest funkcyjnym językiem programowania. Pomimo faktu, iż Java 8 wprowadziła elementy świata funkcyjnego, wciąż nie ma możliwości tworzenia kodu w pełni funkcyjnego w Javie. Jednym z głównych aspektów programowania funkcyjnego jest użycie monad. Nie będę opisywał czym są monady ale przedstawię kilka z nich i odpowiem na pytanie zadane w tytule.

Jak używać monad w Javie? Po prostu dodaj zależności do Vavr do swojego projektu:

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.9.2</version>
</dependency >

Vavr jest biblioteką Javy dostarczającą trwałe typy danych i funkcyjne struktury kontroli. Zawiera zestaw niezmiennych interfejsów Value (kontenerów monadycznych) pomagających pisać kod funkcyjny w Javie. Koncept monad został zaczerpnięty z języków funkcyjnych takich jak Scala.

Option

Option jest interfejsem reprezentującym wartość opcjonalną. Interfejs implementują instancje klas Some oraz None. Instancje Some zawierają wartość (która może być nawet równa null) podczas gdy None jest singletonową reprezentacją niezdefiniowanej wartości Option. Dzięki temu Option spełnia wymaganie utrzymania kontekstu obliczeniowego monady podczas wywołania map(). Oznacza to, że wywołanie map() dla instancji None zwróci zawsze None a dla instancji Some – zawsze Some.

Mogliście słyszeć o tym, że java.util.Optional jest zepsuty. Nie chcę wnikać w szczegóły. Optional w Javie nie spełnia wymagań monad. Możecie dowiedzieć się więcej tutaj https://web.archive.org/web/20160323173020/https://developer.atlassian.com/blog/2015/08/optional-broken/ W Javie kontekst Optional może zmienić się z Some na None podczas wywołania:

optional.map(someString -> (String) null )

W Vavr, Option działa jak Option w Scali. Poza tą różnicą Option ma także bogatsze API niż Optional.

@Test
void optionVsOptional() {
    //java Optional
    Optional<Object> optional = Optional.of(1).map(a -> null);
    //vavr Option
    Option<Object> option = Option.of(2).map(a -> null);
    Option<Object> emptyOption = Option.none();

    assertThat(optional.isPresent()).isFalse();
    assertThat(option.isDefined()).isTrue();
    assertThat(option.get()).isNull();
    assertThat(emptyOption.isDefined()).isFalse();
}

Try

Try jest typem reprezentującym obliczenia które mogą rzucić wyjątek lub zwrócić poprawnie przetworzoną wartość. Interfejs implementują instancje klas Success i Failure. Nie musicie zajmować się wyjątkami oznaczonymi. Pozwala to na pisanie kodu Javy bez sekcji catch. Napiszę o tym więcej w kolejnym artykule.

int countLinesInFile() throws FileNotFoundException {
    throw new FileNotFoundException();
}

@Test
void javaTryCatch() {
    try {
        int lines = countLinesInFile();
        Assertions.fail();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }
}

@Test
void vavrTry() {
    Option<Integer> lines = Try.of(this::countLinesInFile)
            .onFailure(Throwable::printStackTrace)
            .toOption();
}

Lazy

Lazy jest typem reprezentującym dane wyliczane leniwie. Lazy korzysta z memoizacji więc powinien być wykorzystywany tylko z funkcjami czystymi. Metoda opakowana jest wywołana kiedy rezultat jest potrzebny.

@Test
void lazy() {
    Lazy<Double> randomLazyValue = Lazy.of(Math::random);

    assertThat(randomLazyValue.isEvaluated()).isFalse();

    assertThat(randomLazyValue.get()).isEqualTo(randomLazyValue.get());

    assertThat(randomLazyValue.isEvaluated()).isTrue();

Either

Either reprezentuje wartość jednego z dwóch możliwych typów. Jest podobny do typu Tuple który zawiera dwa różne typy ale tylko jeden w danej chwili. Either jest albo typy Left albo Right. Może być używany kiedy chcemy rozróżnić między sukcesem i porażką wywołania funkcji. Następnie możliwe jest wykonanie jednej akcji w przypadku sukcesu a innej w przypadku porażki. Obowiązuje konwencja mówiąca że sukces jest typu Right a porażka Left.

Future

Future opakowuje operacje i dostarcza zestaw nieblokujących operacji na rezultacie. Może znajdować się w stanie pending lub completed. Stan pending oznacza że przetwarzanie trwa. Pozwala on na przerwanie operacji zanim stan zmieni sie na completed. Stan completed oznacza zakończenie przetwarzania sukcesem z rezultatem, porażką z rzuconym wyjątkiem, lub przerwanie przetwarzania. Główną zaletą nad Futute z bazowej Javy jest łatwość rejestrowania funkcji wywołania zwrotnego oraz komponowania operacji w nieblokujący sposób.

private int sleepAndThrowExc() throws InterruptedException {
    Thread.sleep(300);
    throw new IllegalArgumentException();
}

private int sleepAndReturnResult() throws InterruptedException {
    Thread.sleep(100);
    return 1;
}

@Test
void future() {
    Future<Integer> catnap = Future.of(this::sleepAndThrowExc)
            .andThen(res -> System.out.println("1: " + res))
            .andThen(res -> System.out.println("2: " + res))
            .fallbackTo(Future.of(this::sleepAndReturnResult))
            .andThen(res -> System.out.println("3: " + res))
            .andThen(res -> System.out.println("4: " + res))
            .onFailure(throwable -> System.out.println("FAIL"))
            .onSuccess(res -> System.out.println("SUCCESS: " + res));

    assertThat(catnap.isCompleted()).isFalse();
    Integer result = catnap.getOrElse(-1);
    assertThat(catnap.isCompleted()).isTrue();
    assertThat(result).isEqualTo(1);

    // The output will be:
    // 1: Failure(java.lang.IllegalArgumentException)
    // 2: Failure(java.lang.IllegalArgumentException)
    // 3: Success(1)
    // 4: Success(1)
    // SUCCESS: 1
}

Podsumowanie

W tym artykule opisałem kilka kontenerów monadycznych z biblioteki Vavr. Mogą one wyglądać jak lukier składniowy ale poza tym mają one o wiele bogatsze API niż odpowiedniki z Javy. Są także bardzo dobrze zaprojektowane. Java wciąż nie jest językiem funkcyjnym jednak Vavr pozwala nam pisać programy korzystając z cech języków funkcyjnych.

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami