Chrzan z keczupem – własny interceptor Wasabi w Kotlinie

Czerwiec 29th, 2016

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

Czerwiec 24th, 2016

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…

Czerwiec 21st, 2016

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

Czerwiec 20th, 2016

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

Czerwiec 20th, 2016

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

Czerwiec 19th, 2016

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.

Pattern matching w Javie z Javaslang II

Czerwiec 18th, 2016

Poprzednie części:

Dopasowanie wzorców do wartości jest w sumie proste. Podobny efekt można uzyskać stosując mapy. Jednak Javaslang udostępnia też API, w którym dopasowanie oparte jest o predykaty. Przy czym są to predykaty biblioteki, a nie Javy 8.

Listing 1. Dopasowanie z wykorzystaniem predykatu

public void predicates() {
    Integer i = 0;
    String on = Match(i).of(
            Case(x -> x == 1, "Jeden"),
            Case(x -> x == 2, "Dwa"),
            Case($(), "?")
    );
    System.out.println(on);
}

Predykaty z API

Oczywiście API Javaslang udostępnia nam cały zestaw predykatów, których możemy użyć „od ręki”:

Listing 2. Przykładowe predykaty z API

public void predicates2(Object i) {
    String on = Match(i).of(
            Case(is(1), "Jeden"),
            Case(isIn(2, 3), "Dwa albo 3"),
            Case(anyOf(is(4), noneOf(is(5), is(6))), "4 lub nie (5 lub 6)"),
            Case(instanceOf(String.class), "Jakiś napis"),
            Case($(), "?")
    );
    System.out.println(on);
}

Do wyboru, do koloru. Predykaty można łączyć na wiele sposobów. anyOf, noneOf to tylko dwa z kilku dostępnych.

Wartości zwracane

Dotychczas patrzyliśmy tylko na to, jak można zdefiniować dopasowanie. Wartość zwracana z warunku była na sztywno zapisana w kodzie. Zamiast tego można użyć funkcji i Supplierów z API Javy 8:

Listing 3. Przykładowe zwracanie wartości

public void returnig() {
    Integer i = 1;
    String on = Match(i).of(
            Case($(1), v -> v + ""),
            Case($(2), () -> "Dwa"),
            Case($(), "?")
    );
    System.out.println(on);
}

Efektywnie Match zachowuje się jak wyrażenie i musi zwrócić wartość. Co w przypadku gdy zamiast zwracania wartości chcemy wykonać jakąś czynność? W tym przypadku musimy użyć metody pomocniczej run:

Listing 4. Użycie run w celu uruchomienia kodu

public void running() {
    Integer i = 1;
    Void of = Match(i).of(
            Case($(1), () -> run(System.out::println)),
            Case($(2), () -> run(System.out::println)),
            Case($(), () -> null)
    );
}

Krytycznym wymaganiem jest opakowanie run w Supplier. W przeciwnym wypadku zostanie on uruchomiony przed procedurą dopasowania. Dlaczego? Ponieważ ewaluacja parametrów w Javie jest zachłanna.

Podsumowanie

Jak widać, dopasowanie wzorców w wersji Javaslang jest dobrze rozwinięte i pozwala na uzyskanie wielu rozwiązań, które są znane z języków, gdzie mechanizm ten jest częścią samego języka.

Pattern matching w Javie z Javaslang I

Czerwiec 17th, 2016

Gdy kilka dni temu wrzuciłem wpis o tuplach, prawie od razu pojawiły się pytania:

hmmm…. a do czego takie dziwne konstrukcje ? taka sztuka dla sztuki

Samodzielnie krotki rzeczywiście są mało przydatne. Co prawda czasami warto ich używać np. gdy potrzebujemy POJO, a nie można go stworzyć albo chcemy sobie zapewnić kolejność typów w strukturze. Jednak znacznie więcej możliwości dają nam krotki w połączeniu z dopasowaniem wzorców. Dziś zaczniemy omawiać ten mechanizm i jego implementację w Javaslang. Nie jest on rozwiązaniem natywnym, jak na przykład w Kotlinie, ale i tak daje on dużo możliwości.

Dopasowanie dla wartości

To co ułatwi nam życie jest statyczny import całego javaslang.API..

Jeżeli chcemy dopasować jakąś wartość do wzorca by uzyskać np. odpowiedni obiekt możemy użyć ifologii. Proste, łatwe i zrozumiałe, prawda?

Listing 1. Ifologia w praktyce

public void ifology() {
    Integer i = 0;
    String on;
    if (i == 1) {
        on = "Jeden";
    } else if (i == 2) {
        on = "Dwa";
    } else {
        on = "?";
    }
    System.out.println(on);
}

Raptem trzy proste warunki i bardzo dużo kodu. Co będzie w przypadku gdy przetwarzamy plik binarny, blok po bloku i chcemy odpowiednio interpretować nagłówki bloków? No delikatnie mówiąc przejebane. Zamiast tego możemy napisać tak:

Listing 2. Pattern matching dla wartości

public void introduction() {
    Integer i = 0;
    String on = Match(i).of(
            Case($(1), "Jeden"),
            Case($(2), "Dwa"),
            Case($(), "?")
    );
    System.out.println(on);
}

Przy dużej liczbie warunków kod nadal będzie długi, ale znacznie bardziej czytelny. Pary dopasowanie – wartość pozwalają na lepsze zrozumienie kodu.

// offtopic

Zapewne wiele osób powie, że nie warto kombinować, bo ify są łatwiejsze w zrozumieniu. Nie do końca. Problemem jest nieznajomość API, idiomów i konstrukcji z biblioteki. Jeżeli je poznamy, to okazuje się, iż czytanie kodu napisanego tak jak na listingu 2 jest naturalne. Równie dobrze można zarzucić komuś, że pisze niezrozumiały kod w Javie, bo w C# pisze się zupełnie inaczej. Szczytem tego typu dyskusji są te o notacji.

// koniec offtopicu

Dopasowanie tworzymy za pomocą metody $(wartość). Dopasowania są sprawdzane po kolei i pierwsze pasujące zwraca wynik. Metoda $() jest dopasowaniem w rodzaju „dla każdego”. Jeżeli jej nie umieścimy, to otrzymamy błąd MatchError:

Listing 3. Brak dopasowania domyślnego prowadzi do błędu

public void introduction2() {
    Integer i = 0;
    String on = Match(i).of(
            Case($(1), "Jeden"),
            Case($(2), "Dwa")
    );
    System.out.println(on); // nie wyświetli się
}

Jeżeli wiemy, że może dojść do takiej sytuacji, a nie mamy reguł dla domyślnego dopasowania, to możemy z korzystać z opcji:

Listing 4. Dopasowanie i Option

public void introduction3() {
    Integer i = 0;
    Option<String> on = Match(i).option(
            Case($(1), "Jeden"),
            Case($(2), "Dwa")
    );
    System.out.println(on);
}

Wynikiem będzie None. Javaslang używa własnych kontenerów wartości. Warto o tym pamiętać.

Podsumowanie

Mając dopasowania dla wartości, możemy już znacznie ulepszyć nasz kod. Łącząc je z leniwą ewaluacją wartości, można uzyskać całkiem ciekawe rozwiązania niektórych problemów.

Lenistwo ponad wszystko

Czerwiec 16th, 2016

Lenistwo to cnota programisty. Tylko leniwy programista zrobi daną rzecz porządnie za pierwszym razem, ponieważ jako istota leniwa nie będzie chciał wracać do nudnego zadania, a skupić się na czymś ciekawszym.

Javaslang udostępnia nam klasę Lazy, która opakowuje leniwie wyznaczaną wartość.

Motywacja

Podobnie jak w przypadku memoryzacji funkcji chcemy zapamiętać wynik ciężkich obliczeń, by nie musieć ich powtarzać, tak w przypadku leniwej ewaluacji idziemy krok dalej i uruchamiamy obliczenia tylko wtedy gdy naprawdę musimy. Kiedyś już o tym opowiadałem, ale dziś szybko o tym, jak robi to Javaslang.

Kontener Lazy

Pierwszym sposobem w jaki możemy pracować z wartościami leniwymi jest bezpośrednie użycie kontenera Lazy:

Listing 1. Bezpośrednie użycie Lazy

public void container() {
    Lazy<Integer> of = Lazy.of(() -> new Random().nextInt());
    System.out.println(of.get());
    System.out.println(of.get());
}

W tym przypadku pierwsze wywołanie of.get spowoduje, że zostanie wywołana metoda get z Suppliera, wyliczona i zapamiętana wartość, a wynik zwrócony. Kolejne wywołania będą już zwracać zapamiętaną wartość. Takie podejście jest bardzo proste i intuicyjne. Wymaga jednak od nas dodatkowego, jawnego, kroku w postaci wywołania get. Kolejna wada to zmiana typu. Wyobraźmy sobie, że chcemy mieć leniwie wyznaczony obiekt typu T, a mamy tylko Supplier[T]. Wywołanie get na Supplierze jest bez sensu, bo tracimy leniwość. Podobnie będzie w przypadku Lazy. Główną różnicą pomiędzy Lazy w tym wydaniu, a Supplierem jest memoryzacja, ale…

Prawdziwie leniwe obiekty

Lazy dostarcza też mechanizm tworzenia obiektów, o których możemy powiedzieć, iż są prawdziwie leniwe. W tym sensie prawdziwie, że mają swój własny typ, a jednocześnie są wyznaczane leniwie.

Listing 2. Prawdziwie leniwy obiekt

public void trueLazy() {
    SomeInterface of = Lazy.val(() ->  {
	SomeInterface s = () -> new Random().nextInt();
	return s;
    }, SomeInterface.class);
    System.out.println(of);
    System.out.println(of);
}

Podobnie jak w poprzednim przypadku of będzie niezmienne w czasie. Najbardziej widoczna różnica to typ. W przypadku Lazy.val typ zwracanego obiektu jest taki, jakiego dostarcza Supplier. Pytanie, jak? Metoda val zwraca tak naprawdę Proxy, które posiada odpowiedni InvocationHandler, który wywołuje Lazy.get, a ten jak już wiemy, dostarcza odpowiednią wartość.

Podsumowanie

Używanie leniwych wartości jest bardzo dobrym pomysłem. Szczególnie jeżeli wiemy, że ich wyliczenie jest czasochłonne, a nie zawsze będą użyte. Javaslang dostarcza mechanizmów, które pozwalają na łatwe i proste tworzenie tego rodzaju rozwiązań.

Memory z funkcjami

Czerwiec 15th, 2016

Chyba nie pozostaje mi nic innego jak tłuc tematykę Javaslang w najbliższych dniach. Tego typu maratony mają tę zaletę, że można się dużo i szybko nauczyć. Dziś zapamiętywanie wyników działania funkcji. Jest to jeden z idiomów, które można zaimplementować samodzielnie, albo wykorzystać gotowe rozwiązanie.

Funkcja niezmienną jest

Przynajmniej funkcja, która jest czysta, czyli nie ma efektów ubocznych. Nie pisze po dysku lub bazie, nie wyrzuca wyjątków, nie komunikuje się ze światem zewnętrznym, w sposób zmieniający jego stan. Czasami jednak funkcja sięga do czegoś, gdzieś daleko, co też jest niezmienne. Czasami wykonuje bardzo złożone i czasochłonne obliczenia, które dla danych parametrów zawsze dadzą taki sam wynik (rzadki przypadek, ale się zdarza). W takim przypadku warto pomyśleć o zapamiętaniu wyniku w celu użycia go w przyszłości. Skoro będzie on taki sam dla danego zestawu parametrów wejściowych, to czemu by nie. Pozwoli to na zaoszczędzenie kilku… miliardów cykli procesora.

Mechanizm ten zazwyczaj nazywa się cachem. Sama memoryzacjaW, to rodzaj optymalizacji. Na siłę można to podciągnąć pod wzorzec projektowy.

Na czym to polega

Zacznijmy od prostej funkcji, którą chcemy poddać procedurze memoryzacji:

Listing 1. Funkcja wyjściowa

public void memorize() {
    Function1<Integer, String> f = i -> i + " is OK " + new Random().nextInt();
    System.out.println(f.apply(1));
    System.out.println(f.apply(1));
}

Jak widać, za każdym razem otrzymamy inny wynik. To znak, że funkcja została wywołana. Jeżeli twoja funkcja ma zwracać, za każdym razem inny wynik, to nie zapamiętuj wyników! Tu element losowy pełni tylko rolę markera.

Obudujmy naszą funkcję w mechanizm memoryzacji wyniku:

Listing 2. Dodanie memoryzacji

public void memorize() {
    Function1<Integer, String> f = i -> i + " is OK " + new Random().nextInt();
    Function1<Integer, String> mf = f.memoized();
    System.out.println(mf.apply(1));
    System.out.println(mf.apply(2));
    System.out.println(mf.apply(1));
}

Po uruchomieniu zobaczymy, że w przypadku wywołania z parametrem równym 1, wynik pierwszego został zapamiętany i później wyświetlony. Oznacza to, że funkcja f nie została wywołana. Jeszcze jeden przykład obrazujący co się dzieje:

Listing 2. Funkcja z oczekiwaniem na wynik

public void memorize() {
    Function1<Integer, String> f = i -> {
        try {
            Thread.sleep(5000L);
        } catch (InterruptedException e) {

        }
        return i + " is OK " + new Random().nextInt();
    };
    System.out.println(f.apply(1));
    System.out.println(f.apply(1));

    Function1<Integer, String> mf = f.memoized();
    System.out.println(mf.apply(1));
    System.out.println(mf.apply(2));
    System.out.println(mf.apply(1));
}

Pomiędzy dwoma ostatnimi wywołaniami nie ma odczuwalnego opóźnienia.

Jak to działa i armaci

Metoda memorize zwraca nam funkcję wyposażoną w wewnętrzny mechanizm pamięci podręcznej. Jest to prosta mapa, do której dostęp jest synchronizowany. Gdy po raz pierwszy wywołujemy funkcję z danym parametrem, wynik jest zapamiętywany w mapie. Kolejne wywołanie pobierze już obliczony wynik.

Podsumowanie

Mechanizm ten jest znany od dawna i implementowany jako standardowy element w językach funkcyjnych. Choć nie zawsze, bo np. erlang nie ma tego mechanizmu. Zanim jednak hura optymistycznie ruszymy przepisywać nasz kod, należy zwrócić uwagę na dwa zagrożenia.

Pierwszym z nich jest brak mechanizmu unieważniania pamięci podręcznej. Zatem możemy zapomnieć o użyciu tego rozwiązania jako rodzaju cacha wyników z bazy danych. Chyba że nasze rekordy się nie zmieniają. Drugim, znacznie poważniejszym, jest możliwość doprowadzenia do zapełnienia pamięci. Wyobraźmy sobie funkcję, która jako parametr przyjmuje duży obiekt. Zwraca też duży obiekt. Obiekt wejściowy ma błędnie zaimplementowane hashCode i equlas lub jest ich wiele, ale mają w teorii krótki czas życia. W momencie, gdy zostaną zapamiętane to, zamiast zostać usunięte przez GC, będą wisiały w mapie, a my będziemy zastanawiać się, dlaczego serwerowi kończy się pamięć.


Translate »