Słowo o funkcjach anonimowych w Elixirze

Na wczorajszym spotkaniu wroc-fp mieliśmy newsa w postaci „będzie grupa elixirowa” i tak przy okazji padło pytanie, jak działa operator & w elixirze.

Podstawy

Jak wiadomo, chociażby z tego artykułu, jedną z cech praktycznego programowania funkcyjnego jest możliwość przekazywania funkcji jako parametrów i zwracania ich jako wyników. Czasami potrzebujemy wykonać pewną operację przyjmującą jako argument funkcję, ale nie mamy odpowiedniej funkcji w API. W takim wypadku mamy dwie drogi. Pierwsza to utworzenie funkcji w module wraz ze wszystkimi tego konsekwencjami np. związanymi z metrykami, czy sposobem przekazywana funkcji jako parametru. Druga to przekazanie funkcji anonimowej.

Znak & jest powiązany z tą drugą ścieżką.

Funkcja anonimowa

Funkcja anonimowa w elixirze jest definiowana za pomocą konstrukcji fn -> end i może być przypisana do zmiennej:

Listing 1. Definiowanie funkcji anonimowej

iex> multipla = fn (a, b) -> a * b end
#Function<12.50752066/2 in :erl_eval.expr/5>
iex> multipla.(2, 2)
4

Wywołanie takiej przypisanej funkcji jest lekko dziwne, to przez kropkę, ale ma sens, jeżeli chcemy zasygnalizować, że multipla nie jest zwykłą nazwaną funkcją, a właśnie anonimową. Jak już wspomniałem, funkcję można przekazać jako parametr i zwrócić jako wynik:

Listing 2. Funkcja anonimowa jako parametr i wynik

iex>  b2 = fn (f2) -> fn (a) -> f2.(a, 2) end end
#Function<6.50752066/1 in :erl_eval.expr/5>
iex> multiplav2 = b2.(multipla)
#Function<6.50752066/1 in :erl_eval.expr/5>
iex> multiplav2.(2)
4

Działa i armaci. Jednak ten zapis ma pewną, drobną wadę. Jest słabo czytelny, gdy zaczynamy robić coś bardziej skomplikowanego.

Znak &

Odpowiedzią na problem czytelności jest użycie skrótowego zapisu wykorzystującego &:

Listing 3. Definiowanie funkcji anonimowej za pomocą &

iex> mlp = &( &1 * &2)
&:erlang.*/2
iex> mlp.(2, 2)
4

Mamy tu do czynienia z dwoma zastosowaniami znaku &. Pierwsze to oczywiście &(), które odpowiada fn -> end, czyli służy do zdefiniowania funkcji. Drugie to &1 i &2, które reprezentuje parametry funkcji. Jako że skrócona definicja funkcji nie zawiera parametrów nazwanych, to jasne jest, że musi istnieć jakiś sposób dostępu do nich. Zapis ze znakiem & jest takim właśnie sposobem.

Ograniczenia

Zapis ten ma pewne ograniczenia. Pierwszym jest brak możliwości stworzenia funkcji, która będzie wykonywać blok kodu:

Listing 4. & nie pozwala na „duże” funkcje

iex> &(
...> l =[&1, &2]
...> Enum.each(l , fn (el)-> IO.puts el end)
...> )
** (CompileError) iex: invalid args for &, block expressions are not allowed, got: (
  l = [&1, &2]
  Enum.each(l, fn el -> IO.puts(el) end)
)

iex> fn (a, b) ->
...> l = [a, b]
...> Enum.each(l, fn (el) -> IO.puts el end)
...> end
#Function<12.50752066/2 in :erl_eval.expr/5>

Jest to dobre ograniczenie. Dzięki niemu unikamy tego, co pojawiło się w Javie wraz z lambdami, czyli długich anonimowych bloków kodu nazwanych dla niepoznaki lambdami.

Drugim ograniczeniem jest brak możliwości zagnieżdżania się funkcji zapisanych za pomocą &:

Listing 5. Funkcje nie mogą się zagnieżdżać

iex> by2 = &( &( &1.(&1, 2)))
** (CompileError) iex: nested captures via & are not allowed: &(&1.(&1, 2))
    (elixir) src/elixir_fn.erl:114: :elixir_fn.do_capture/4

Oczywiście to też jest rozsądne, ponieważ nie mamy możliwości określenia, który parametr przynależy do której funkcji.

Podsumowanie

Podsumowując nasze dzisiejsze rozważania, możemy powiedzieć, że Elixir pozwala na definiowanie funkcji anonimowych. Mogą one być przypisane do zmiennej, mogą być parametrem, jak i wynikiem wywołania innej funkcji. Ze względu na intensywne użycie funkcji anonimowych mamy możliwość zapisania ich w skrócony sposób z wykorzystaniem znaku &. Zapis skrócony ma pewne ograniczenia. Z drugiej strony promuje on pisanie bardzo krótkich i zwięzłych funkcji.

Java 9 nadchodzi – prywatne metody w interfejsach

Jedną z dużych zmian, jakie przyniosła ze sobą Java 8, było dopuszczenie implementacji metod w interfejsach. Używając słowa kluczowego default, możemy zdefiniować metodę, która będzie mieć implementację:

Listing 1. Przykładowy interfejs z implementacją z Javy 8

interface SomeService{

    default void validate(Client client){
        Preconditions.checkNotNull(client);
    }

    default void someLogic(Client client){
        validate(client);
        TransformedClient transformedClient = transform(client);
        emit(transformedClient);
    }

    default void emit(TransformedClient transformedClient){
        System.out.println(transformedClient);
    }

    TransformedClient transform(Client client);
}

Oczywistym zastosowaniem jest przygotowanie metod szablonowych. Nasz interfejs realizuje pewną większą logikę, która wymaga byśmy w ramach implementacji, dostarczyli jedynie fragmentu logiki reprezentowanego przez metodę someLogic. Tyle tylko, że zarówno validate jak i emit nie powinny być zmieniane (takie założenie). Na poziomie interfejsu jedynym rozwiązaniem, które nam to może zagwarantować, jest zamiana tych metod na statyczne. Nie możemy utworzyć metod finalnych w interfejsach. Jednak nadal pozostaje problem ujawnienia części implementacji. Klienta nie powinno interesować, w jaki sposób przeprowadzana jest walidacja ani jak są emitowane zdarzenia. Z doświadczenia wynika, że te dwie metody prędzej czy później staną się rozwiązaniem zastępczym dla jakiegoś kawałka kodu, gdzieś w odległym miejscu systemu. Skoro dostarczają tych mechanizmów, to czemu by z nich nie korzystać, zamiast zrobić to porządnie, na przykład wydzielając odpowiednią klasę.

A jak w Javie 9?

Java 9 dostarcza nam znacznie lepszy mechanizm, który pozwala na rozwiązanie tego problemu:

Listing 2. Przykładowy interfejs z prywatnymi metodami z Javy 9

interface SomeService{

    private void validate(Client client){
        Preconditions.checkNotNull(client);
    }

    default void someLogic(Client client){
        validate(client);
        TransformedClient transformedClient = transform(client);
        emit(transformedClient);
    }

    private void emit(TransformedClient transformedClient){
        System.out.println(transformedClient);
    }

    TransformedClient transform(Client client);
}

I to wszystko. Otrzymujemy zgrabny kod, który ma ukryte, to co powinno być ukryte i ujawnia tylko, to co trzeba.

Podsumowanie

Dziedziczenie wielobazowe zwane też wielodziedziczeniem w Javie jest realizowane za pomocą interfejsów. Dzięki temu nie mamy problemów związanych z konfliktami na poziomie implementacji. Wprowadzenie metod z domyślną implementacją w Javie 8 wymogło wprowadzenie kilku reguł dotyczących nadpisywania metod i rozwiązywania konfliktów w przypadku takich samych sygnatur. W dużym skrócie opierają się one, na wymuszeniu na programiście jasnej deklaracji co chce zrobić.
Java 9 wprowadza do tego mechanizmu rozwiązania, które pozwalają na uniknięcie części konfliktów, poprzez ukrycie niektórych metod. Ważniejsze jest jednak uzyskanie możliwości ukrywania elementów interfejsu. Dzięki temu zachowana będzie hermetyzacja naszego rozwiązania na odpowiednim poziomie. Zagrożeniem może okazać się wykorzystywanie interfejsów zamiast klas abstrakcyjnych. Szczególnie tam, gdzie klasy abstrakcyjne nie miały właściwości (pól). W ten sposób powstaną potworki, które będą miały wiele niepowiązanych odpowiedzialności.

Odpowiedzi po Confiturze

W sumie odpowiedź na pytanie o biblioteki, które trzeba mieć w swoim arsenale, jeżeli ruszamy do boju przeciwko Javie 8 🙂

Na podstawie tego tekstu plus moje rozszerzenia:

  • Google Guava – Nie tylko kolekcje, ale też ESB, net, czy grafy (o tym nie pisałem). Tam jest dużo różnych ciekawych rzeczy ponad kolekcjami.
  • Lombok – generatory kodu, obiekty niezmienne itp. Robienie sobie dobrze na poziomie objętości kodu. Przy czym trzeba uważać, bo dookoła śmiga też JPA czy Spring ze swoimi proxy i aspektami.
  • pCollections – kolekcje, które czasami się przydają. Nie zastąpią tych z API, ale czasami potrzebujemy specyficznych funkcjonalności czy też niezmienności. To jest jakiś pomysł.
  • Javaslang – różne elementy funkcyjne, dość ciekawe strumienie i duża naturalność w pisaniu. Przy czym z tyłu głowy pytanie o wydajność… ale to jest Java 😉
  • JOOQ/JOOL – biblioteka, która ma podobne zadanie co pCollection. Generalnie czasami potrzebujemy jakiegoś ekstra API dla strumieni. Tu je zapewne znajdziemy.

Miła pani, która o to zapytała – kto ty?

Pytanie od Tomka Nurkiewicza o użycie parallel streamów. Wersja krótka – wątki potrzebują RAMu, którego nie mogliśmy zapewnić. Wersja trochę bardziej rozbudowana. „Optymalizacje” zrównoleglające mają taki dziwny „defekt”, że JVM próbuje alokować pamięć dla każdego wątku, ale nie swapując pamięci wątków nieaktywnych. Inaczej mówiąc, jeżeli chcemy za alokować np. 500MB pamięci dla każdego z 10 wątków (zakładając, że możemy puścić 10 wątków na raz) to potrzebujemy 5GB i jeżeli damy maszynie 2GB to się wyjebie (albo nie wiem jak to skonfigurować). Dlatego też trzeba delikatnie z mechanizmami wielowątkowymi.

Tyle na dziś.

10 lat Confitury i to już jest koniec…

Długie dni mają to do siebie, że są długie. Start z mieszkania przed 6 i już o 11 byłem na miejscu, czyli w centrum konferencyjnym na Bobrowieckiej. Tam gdzie była pierwsza Confitura (2011), pod nazwą Confitura właśnie. Tym razem dane mi było obejrzeć tylko dwie prezentacje, ale mogłem poprowadzić trzecią 🙂 Cieszę się, że głosowaliście i zapewniliście mi tę możliwość.

Concurrency in Java – Mateusz Kaczmarek

Myśl przewodnia tej prezentacji i coś, o czym warto pamiętać. Nasz software nie żyje w próżni. JVM, system, krzem to wszystko należy brać pod uwagę, gdy piszemy kod współbieżny. Całkiem fajna prezentacja, rzeczowa i pozostawiająca pewien niedosyt. Warto było posłuchać.

Kotlin, why? – Paweł Byszewski

Paweł na przykładzie z życia pokazał, jakie są przewagi Kotlina nad „czystą” Javą. Całkiem fajna prezentacja, w której prawie nic nie zabrakło. Z drugiej strony mam wrażenie, że to „prawie”, to całkiem duży zakres materiału. Szczególnie że pytania z sali dotyczyły też składni. Z drugiej strony mając mało czasu, trzeba coś ciąć. Na pewno prezentacja ta pokazała mi, o jakich rzeczach warto wspominać, gdy omawiamy Kotlina, bo była prowadzona z punktu widzenia programisty androidowego.

To już jest koniec

Całe słuchanie ciężkich brzmień w moim wypadku zaczęło się od jakiejś kasety ze składanką Black Sabbath. Wcześniej było oczywiście Deep Purple, Pink Floyd czy Queen, ale to nie było jeszcze to. Niedługo później poznałem ból zakupu oryginalnych płyt… Paranoid kosztowało jakieś 69PLN… cóż. Wakacje upłynęły mi pod znakiem Electric Funeral, War Pigs i Fallouta 🙂

Koncert Black Sabbath był świetny. Jednak zawsze po tego typu wydarzeniach pozostaje pewien niedosyt. Poczucie, że mogli zagrać jeszcze jeden numer. Jednak i tak było świetnie. Szkoda, że był to ostatni koncert i nie będzie już okazji.

Chrzan z keczupem – własny interceptor Wasabi w Kotlinie

Tydzień odpoczynku starczy. Można wrócić do pisania 🙂 Dziś zaimplementujemy własny interceptor w frameworku Wasabi.

Czym jest Wasabi?

Wasabi to framework HTTP napisany w Kotlinie. Pozwala na tworzenie aplikacji wykorzystujących protokół http jako warstwy komunikacji. Posiada wiele elementów, które pozwalają go kwalifikować jako narzędzie REST, ale nie jest na pewno frameworkiem REST. Można tworzyć rozwiązania zorientowane na zasoby, ale nie to jest głównym zadaniem.

Cały mechanizm działania opiera się o dobrze znane elementy takie jak routing, kanały (channels), metody HTTP. Całość jest postawiona na nettym.

Czym są interceptory?

Jak sama nazwa wskazuje, służą one do przechwytywania żądań, które przychodzą do serwera. Następnie wykonywana jest pewna logika związana z żądaniem i wynik jest zwracany jako odpowiedź serwera, następuje przerwanie przetwarzania, albo przesyłany do dalszej obróbki. Można o nich myśleć jak o filtrach znanych z aplikacji webowych. Choć są trochę bogatsze, jeśli chodzi o możliwości.

Jednym z przypadków użycia interceptora jest obsługa statycznych zasobów takich jak statyczne, z punktu widzenia serwera, strony html, pliki js, css, obrazki. Innym jest obsługa nagłówków, autoryzacji, negocjacja dodatkowych parametrów połączenia itp.

Nasz mały interceptor

Wasabi ma na pokładzie klasę StaticFileInterceptor, która pozwala na obsługę statycznych zasobów. Ma on jednak małą wadę w postaci nie możności obsługi domyślnego pliku dla katalogu. Inaczej mówiąc, jeżeli żądanie wskazuje na zasób, który jest katalogiem to dostajemy 404, o ile gdzieś dalej nie leży mapowanie pasujące do wskazanego zasobu, a nie tak jak w przypadku serwerów www plik index.XXX.

Zgłosiłem odpowiednią poprawkę, która została już wdrożona, ale chciałbym omówić, jak do niej doszedłem.

Najpierw kod oryginalnego interceptora:

Listing 1. StaticFileInterceptor stan wyjściowy

public class StaticFileInterceptor(val folder: String): Interceptor() {
    override fun intercept(request: Request, response: Response): Boolean {
        var executeNext = false
        if (request.method == HttpMethod.GET) {
            val fullPath = "${folder}${request.uri}"
            val file = File(fullPath)
            if (file.exists() && file.isFile()) {
                response.setFileResponseHeaders(fullPath)
            } else {
                executeNext = true
            }
        } else {
            executeNext = true
        }
        return executeNext
    }
}

public fun AppServer.serveStaticFilesFromFolder(folder: String) {
    val staticInterceptor = StaticFileInterceptor(folder)
    intercept(staticInterceptor)
}

Mamy tu do czynienia z dwoma elementami. Pierwszy z nich, to implementacja interfejsu Interceptor. Metoda intercept kieruje dalszym przetwarzaniem żądania w następujący sposób. Jeżeli zwróci true, to żądanie jest przekazywane do mechanizmu routingu. Jeżeli zwróci false, to żądanie jest kończone i odsyłane, tak jak jest, co oznacza, że obiekt Response jest przekazywany do mechanizmu zamieniającego go na odpowiedni obiekt z nettiego, który dalej już robi robotę sieciową. Drugim elementem jest funkcja rozszerzająca, która jest dodana do AppServer i obudowuje mechanizm tworzenie i dodawania interceptora.

Wadę tego rozwiązania już opisałem wyżej. Przejdźmy więc do naszej implementacji, która jest trochę inna niż ta, która poszła do repozytorium, ponieważ znacznie intensywniej wykorzystuje wyrażenie when, które nie wszyscy lubią.

Listing 2. Nasz interceptor

class CustomStaticFileInterceptor(val folder: String, val useDefaultFile: Boolean = false, val defaultFile: String = "index.html") : Interceptor() {

    private fun existingDir(path: String): Boolean {
        val file = File(path)
        return file.exists() && file.isDirectory
    }

    private fun existingFile(path: String): Boolean {
        val file = File(path)
        return file.exists() && file.isFile
    }

    override fun intercept(request: Request, response: Response): Boolean {
        return when (request.method) {
            HttpMethod.GET -> {
                val fullPath = "${folder}${request.uri}"
                when {
                    existingFile(fullPath) -> {
                        response.setFileResponseHeaders(fullPath); false
                    }
                    existingDir(fullPath) && useDefaultFile -> {
                        response.setFileResponseHeaders("${fullPath}/${defaultFile}"); false
                    }
                    else -> true
                }
            }
            else -> true
        }
    }
}

fun AppServer.serveStaticFiles(folder: String, useDefaultFile: Boolean = false, defaultFile: String = "index.html") {
    val staticInterceptor = CustomStaticFileInterceptor(folder, useDefaultFile, defaultFile)
    this.intercept(staticInterceptor)
}

W tej wersji mamy kilka drobnych zmian. Po pierwsze dodałem dwa pola useDefaultFile oraz defaultFile, które mają domyślne wartości. Pozwoli nam to zachować wsteczną kompatybilność API. Następnie dopasowujemy warunki działania interceptora. Jeżeli mamy do czynienia z żądaniem innym niż GET, to zwracamy true, co pozwala na kontynuowanie przetwarzania (ostatni else). Jeżeli jednak mamy do czynienia z żądaniem GET, to mamy trzy możliwości. Pierwsza żądanie dotyczy istniejącego pliku, wtedy budujemy odpowiedź i zwracamy false. Druga to sytuacja, gdy żądanie dotyczy katalogu i mamy włączoną obsługę plików domyślnych. Wtedy budujemy odpowiedź z domyślnym plikiem, bez sprawdzania, czy plik istnieje – jego brak to błąd konfiguracji i nas nie obchodzi, i zwracamy false. Trzecia sytuacja reprezentowana przez else, to żądanie katalogu przy wyłączonej obsłudze plików. Zwracamy true i tyle.

W celu zachowania spójności musimy też dodać funkcję do AppServer. W zgłoszonej poprawce zmieniamy istniejącą klasę, a zatem, zamiast dodawać funkcję, zmieniamy ją. Ostatnim krokiem jest likwidacja powtórzeń na poziomie wartości domyślnych. Wystarczy je wyekstrahować do prywatnych stałych w pliku:

Listing 3. Ekstrakcja stałych i nagłówki

private val DEFAULT_USE_DEFAULT_FILE = false
private val DEFAULT_DEFAULT_FILE = "index.html"

class CustomStaticFileInterceptor(val folder: String, val useDefaultFile: Boolean = DEFAULT_USE_DEFAULT_FILE, val defaultFile: String = DEFAULT_DEFAULT_FILE) : Interceptor()
fun AppServer.serveStaticFiles(folder: String, useDefaultFile: Boolean = DEFAULT_USE_DEFAULT_FILE, defaultFile: String = DEFAULT_DEFAULT_FILE)

Podsumowanie

Wasabi framework jest bardzo młodym rozwiązaniem, ale świetnie nadaje się jako miejsce gdzie połączyć naukę języka z rozwojem rzeczywistego oprogramowania. Dzisiejszy przykład pokazuje jak łatwo w kotlinie jest stworzyć rozszerzenie do istniejącego rozwiązania, ponieważ język wymusza na twórcach elastyczność w tworzeniu kodu.

Szkolenie z Kotlina runda druga

Robimy powtórkę, bo zainteresowanie 🙂

REJESTREACJA

Podsumowanie I edycji

Mam nadzieję, że uczestnicy są zadowoleni. Co prawda trzy godziny to trochę za mało czasu. Forma ćwiczeń po przetestowaniu jej w boju też nie spełniła wszystkich moich oczekiwań, ale było nieźle. Zatem druga grupa będzie miała ten sam materiał, ale w trochę innej formie. Nie będą to duże różnice. Poprawimy kilka drobnostek, które wyszły w praniu.

Zwiększyliśmy też limit miejsc do 30 (jak to piszę to może jeszcze nie być tej informacji na stronie). A teraz szczegóły.

Gdzie?

Wrocław, pl. Konstytucji 3 Maja 3, budynek Silver Tower 13 piętro (zajebisty widok na Wrocław):

Wejście od ulicy Dąbrowskiego, drzwi obok krasnoludka.

Kiedy?

30 czerwca startujemy o 17:30 od czegoś na ciepło. cały czas będą dostępne przekąski na zimno i kawa na zimno z G Coffee Company.

Co i jak?

Będzie to wprowadzenie do Kotlina. Dlatego potrzebujecie własne laptopy z zainstalowanym:

  • Ulubionym IDE
  • Klientem git
  • Gradle
  • Javą 1.8

Całość potrwa około 3 godzin. Zakończenie planowane jest na 21. Później możemy pójść na piwo 🙂

REJESTREACJA

Już lato…

Szybko minęło. W sumie 96 wpisów w ramach blogowej wiosny… Gdy zabierałem się za ten cykl, to nie zauważyłem, że będzie to około 10% wszystkich wpisów na blogu. To było dużo pracy. Były święta i długie weekendy. Jeździliśmy po Polsce i zwiedzaliśmy. Czasami trzeba było „wyprodukować” kilka tekstów w przeciągu jednego dnia. Jednak udało się.

Co dalej?

Wiele razy słyszałem, że szata graficzna bloga jest zła. Mam tego świadomość i plany, by coś z tym zrobić. Może przez wakacje uda się jakoś rozwiązać ten problem. Łatwo nie będzie, bo treść jest specyficzna, co czyni układ graficzny trudnym do zaprojektowania.

Inna sprawa to korekta starszych wpisów. Szczególnie pod kątem poprawności listingów, ich formatowania i organizacji. Tu też czeka mnie dużo pracy.

Nadal otwartym tematem jest napisanie kilku pluginów do obsługi listingów. Szczególnie jeśli chodzi o wstawianie kodu do treści, bo to kuleje najbardziej.

Co teraz?

Zapewne kilka dni odpoczynku od pisania. Muszę przygotować kurs Kotlina, którego wersja skrócona już pojutrze.

uf… dało się 🙂

Szkolenie z Kotlina

Małe, 3 godzinne szkolenie z Kotlina już w ten czwartek 🙂

REJESTREACJA

Gdzie?

Wrocław, pl. Konstytucji 3 Maja 3, budynek Silver Tower 13 piętro 13 piętro (zajebisty widok na Wrocław):

Wejście od ulicy Dąbrowskiego, drzwi obok krasnoludka.

Kiedy?

23 czerwca (i później zapewne będzie powtórka) startujemy o 17:30 od lunchu, zatem jak chcesz coś wszamać, to trzeba być punktualnie.

Co i jak?

Będzie to wprowadzenie do Kotlina. Dlatego potrzebujecie własne laptopy z zainstalowanym:

  • Ulubionym IDE
  • Klientem git
  • Gradle
  • Javą 1.8

Całość potrwa około 3 godzin. Zakończenie planowane jest na 21. Później możemy pójść na piwo 🙂

Ogólne założenie jest takie, że zaimplementujemy rozwiązania kilku problemów, tak by poznać język.

REJESTREACJA

Pattern matching w Javie z Javaslang IV

Poprzednie części:

Początkowo chciałem włączyć temat tworzenia własnych wzorców do poprzedniego wpisu. Jednak ze względu na konieczność zrobienia kilku rzeczy, bez których będzie nam ciężko, postanowiłem wyłączyć ten temat do osobnego wpisu.

Dodatkowe zależności

Samodzielne pisanie wzorców jest uciążliwe i w sumie mało przyjemne. Javaslang jest jednak oparty o generatory, a zatem pozwólmy robić im ich robotę. W tym celu musimy dodać jedną zależność w naszym pliku build.gradle:

Listing 1. Konfiguracja projektu

apply plugin: 'java'

repositories {
    jcenter()
    mavenCentral()
}

sourceSets {
    generated {
        java {
            srcDirs = ['src/main/java']
        }
    }
}

configurations {
    patternMatch
}

dependencies {

    compile 'org.slf4j:slf4j-api:1.7.21'
    compile('io.javaslang:javaslang:2.0.2')
    patternMatch 'io.javaslang:javaslang-match:2.0.2'

    testCompile 'junit:junit:4.12'
}

task generatePatterns(type: JavaCompile, group: 'build', description: 'Generates patterns classes') {

    source = sourceSets.main.java 
    classpath = configurations.compile + configurations.patternMatch
    options.compilerArgs = [
            "-proc:only",
            "-processor", "javaslang.match.PatternsProcessor"
    ]
    destinationDir = sourceSets.generated.java.srcDirs.iterator().next()
}

compileJava {
    dependsOn generatePatterns

    options.compilerArgs =[
            "-proc:none"
    ]
}

compileGeneratedJava {
    dependsOn generatePatterns
    options.warnings = false
    classpath += sourceSets.main.runtimeClasspath
}

Biblioteka javaslang-match zawiera procesor adnotacji, który wygeneruje nam odpowiednie klasy. Jednak by prawidłowo działał, należy:

  • Stworzyć dodatkowy task, który zajmie się generowaniem klas.
  • Wyłączyć procesory adnotacji w głównym cyklu kompilacji.

Pierwsze robimy poprzez dodanie zadania generatePatterns oraz konfiguracji patternMatch. Drugie za pomocą dodania argumentu -proc:none do wywołania kompilatora. Powyższa konfiguracja to kombinacją braku możliwości konfiguracji generatora i mojej niewiedzy w zakresie konfigurowania Gradle.

Model

Tu sprawa jest prosta. Będziemy używać klasy User, która zawiera trzy pola:

Listing 2. Klasa User

public class User {

    private String name;
    private String email;
    private String role;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }
}

Ot zwykłe POJO. Równie dobrze możemy użyć data class z Kotlina. Jednak jest jeden warunek. Jeżeli nie chcemy samodzielnie kompilować Javaslang, to musimy ograniczyć się do maksymalnie ośmiu pól w dekompozycji obiektu. Tyle właśnie wynosi największa pojemność krotki w Javaslang.

Własny wzorzec

Tworzenie własnego wzorca rządzi się prostymi zasadami. Po pierwsze musimy stworzyć klasę, która będzie zawierać wzorce. Oznaczymy ją adnotacją @Patterns.

Listing 3. Klasa Users

@Patterns
public class Users {
 
}
T’n’T z nazwami

Generator utworzy klasę UsersPatterns. Dlaczego nie możemy utworzyć klasy User? Jest bug w Javaslang, który powoduje, że generator nie używa pełnych nazw klas wraz z pakietami. W efekcie w pewnym momencie mamy sytuację jak poniżej:

Listing 4. Błędnie wygenerowana klasa

package pl.koziolekweb.javaslang.patternmatching.patterns;

import javaslang.API.Match.Pattern;
import javaslang.API.Match.Pattern3;
import pl.koziolekweb.javaslang.patternmatching.User;

// GENERATED BY JAVASLANG <<>> derived from pl.koziolekweb.javaslang.patternmatching.patterns.User

public final class UserPatterns {

    private UserPatterns() {
    }

    public static <_1 extends String, _2 extends String, _3 extends String> Pattern3<User, _1, _2, _3> User(Pattern<_1, ?> p1, Pattern<_2, ?> p2, Pattern<_3, ?> p3) {
        return Pattern3.of(User.class, p1, p2, p3, User::User);
    }

}

Problemem jest zapis User::User, który odwołuje się do naszej klasy ze wzorcami. Jednocześnie mamy zaimportowaną klasę pl.koziolekweb.javaslang.patternmatching.User, co prowadzi do konfliktu nazw. W efekcie mamy błąd kompilacji…

Zgłosiłem buga. Zobaczymy co z nim zrobią. Rozwiązali go.

Dekonstrukcja

Kolejnym krokiem jest dodanie metody, która będzie dekonstruować obiekt User do Tuple3. Z dekonstrukcją na poziomie semantyki języka mieliśmy już do czynienia w Kotlinie. Choć brzmi to groźnie, to sama implementacja jest banalnie prosta:

Listing 5. Metoda User służy do dekonstrukcji

@Patterns
public class Users {
 
    @Unapply
    static Tuple3<String, String, String> User(pl.User user) {
        return Tuple.of(user.getName(), user.getEmail(), user.getRole());
    }

}

Metoda musi mieć adnotację @Unapply i musi być statyczna. Możemy używać naszego wzorca:

Listing 6. Użycie User w dopasowaniu

public void custom() {
    User user = new User("Jan Kowalski", "jan@kowalski", "Jan");

    System.out.println(
            Match(user).of(
                    Case(User($(), $(), $("Jan")), "OK - Jan"),
                    Case(User($(), $(), $("Emil")), "OK - Emil")
            )
    );
}

W efekcie mamy możliwość dopasowania obiektu do wzorca według samodzielnie zdefiniowanych reguł.

Podsumowanie

Tworzenie własnych wzorców i reguł dopasowania nie jest trudne. Najwięcej czasu zajęło mi odpowiednie skonfigurowanie gradle tak by generowane klasy były widoczne w projekcie. To jest jakaś porażka niestety.

Pattern matching w Javie z Javaslang III

Poprzednie części:

Dopasowania wartości z pierwszej części oraz predykatów z drugiej, to nie jedyne możliwości, jakie daje nam Javaslang. Dziś przyjrzymy się wzorcom, czyli specjalnym obiektom, które opisują proces dekonstrukcji danego obiektu tak, by można było go wykorzystać w dopasowaniu. Poznaliśmy już wzorce, które mają znaczenie „każdy pasuje” – $(), oraz jest „równy w sensie equlas” – $(X). Standardowe API daje nam jednak kilka innych możliwości.

Sukces czy porażka

Mamy sobie kontener Try, o którym innym razem. Może mieć dwa stany – sukces (Success) i porażka (Fail). Jest to idealny kandydat do obsługi przez mechanizm dopasowań:

Listing 1. Dopasowanie Try.

public void successOrFailure() {
    Try<Integer> success = Try.of(() -> 1);
    Try<Integer> fail = Try.of(() -> {
        throw new Exception("Ni huhu");
    });

    System.out.println(
            Match(success).of(
                    Case(Success($()), "OK"),
                    Case(Failure($()), "NOK")
            )
    );

    System.out.println(
            Match(fail).of(
                    Case(Success($()), "OK"),
                    Case(Failure($()), "NOK")
            )
    );
}

Jako że Try ma tylko dwa stany to możemy pominąć warunek związany z dopasowaniem $() na końcu. Oczywiście możemy udoskonalić nasz kod, stosując bardziej precyzyjne dopasowania:

Listing 2. Dopasowanie Try z wykorzystaniem wzorców wewnętrznych.

public void successOrFailure() {
    Try<Integer> success = Try.of(() -> 1);
    Try<Integer> fail = Try.of(() -> {
        throw new IllegalArgumentException("Ni huhu");
    });

    System.out.println(
            Match(success).of(
                    Case(Success($(1)), "1 is OK"),
                    Case(Success($(i -> i == 2 )), "2 is OK too"),
                    Case(Failure($()), "NOK")
            )
    );

    System.out.println(
            Match(fail).of(
                    Case(Success($()), "OK"),
                    Case(Failure($(instanceOf(IllegalArgumentException.class))), "NOK - IAE"),
                    Case(Failure($()), "NOK")
            )
    );
}

Dopasowania krotek

Czyli mięsko, o które mnie pytano. Skoro można tworzyć wzorce dla takiego Try, to co stanie się, gdy zaczniemy używać wzorców dla Tuple?

Listing 3. Dopasowanie krotki

public void tuplePattern() {
    Tuple3<String, String, String> of = Tuple.of("Ala", "nie ma", "kota");

    System.out.println(
            Match(of).of(
                    Case(Tuple3($(), $("ma"), $()), "Wiedźma!"),
                    Case(Tuple3($(), $("nie ma"), $()), "Pożeraczka kotów!")
            )
    );
}

W powyższym przykładzie mamy dodatkowo ukryte dopasowanie typów. Wszystkie wzorce muszą być typu Pattern.Tuple3. Technicznie wzorzec jest funkcją, która zwraca typ zgodny z typem obiektu dopasowywanego. Dlatego nie może być to zwykłe Tuple3. Jednocześnie nie możemy w żaden sposób pracować z krotkami o różnej liczności. Oczywiście rozwiązaniem jest użycie typu Tuple dla zmiennej of.

Listing 4. Dopasowanie dowolnej krotki

public void tuplePattern() {
    Tuple3<String, String, String> of = Tuple.of("Ala", "nie ma", "kota");

    System.out.println(
            Match(of).of(
                    Case(Tuple3($(), $("ma"), $()), "Wiedźma!"),
                    Case(Tuple4($(), $("nie"), $("ma"), $()), "Pożeraczka kotów!!!"),
                    Case(Tuple3($(), $("nie ma"), $()), "Pożeraczka kotów!")
            )
    );
}

Oczywiście przekazanie krotki, która do niczego nie pasuje spowoduje wyjątek.

Podsumowanie

Mechanizm dopasowania wzorców jest bardzo elastyczny i umożliwia nam zupełnie inne podejście do tworzenia kodu. Oznacza to, że lepiej możemy dzielić logikę i dane. W połączeniu z innymi elementami mamy naprawdę silne narzędzie.