niedziela, 15 października 2017

Fajny nowy acz niepozorny operator z Javy 9

I nie chodzi o moduły a o nową ciekawą metodę Optional, która zwię się or i jest dostępna od Javy 9.

To skoro small talk mamy za sobą to dlaczego ona taka fajna?

Unia opcjonalnych typów w dynamicznych strukturach

Taka sytuacja. Masz jakiś kanał komunikacji i otrzymujesz przez ten kanał opcjonalną wartość jakiegoś typu. I teraz ten typ którego wartość jest opcjonalna może przyjąć jedną z na przykład 4 elementowego zbioru wartości gdzie każdy element może mieć różną liczbę różnych atrybutów. Na rysunku duże kółko to symbolika typu abstrakcyjnego a małe kółka to podtypy - i technicznie wszystko jest tak opcjonalne, że ani nie wiadomo czy jakaś wartość będzie przekazana a jeśli będzie to jaka to wartość będzie?

Gdy już mało mówiący opis abstrakcyjny mamy za sobą teraz będzie trochę konkretów w stylu retro.

Retro przykład

Żeby nie było, że tylko nowe nowe nowe to zerknijmy zatem na przykład pamiętający czasy monitorów monochromatycznych

Poniżej kawałek jakiejś tam definicji jednego z elementów w jakiejś tam specyfikacji. Nieważne. Ważny jest ten choice który sprawia, że tam w tym typie może być w sumie cokolwiek.

 Schema Definition

   <element name="X509Data" type="ds:X509DataType"/> 
   <complexType name="X509DataType">
     <sequence maxOccurs="unbounded">
       <choice>
         <element name="X509IssuerSerial" type="ds:X509IssuerSerialType"/>
         <element name="X509SKI" type="base64Binary"/>
         <element name="X509SubjectName" type="string"/>
         <element name="X509Certificate" type="base64Binary"/>
         <element name="X509CRL" type="base64Binary"/>
         <any namespace="##other" processContents="lax"/>
       </choice>
     </sequence>
   </complexType>

   <complexType name="X509IssuerSerialType"> 
     <sequence> 
       <element name="X509IssuerName" type="string"/> 
       <element name="X509SerialNumber" type="integer"/> 
     </sequence>
   </complexType>

Żródło : https://www.w3.org/TR/xmldsig-core/ <--- jakiś tam losowy standard sprzed 10 lat wybrany losowo na potrzeby artykułu.

No i są takie patenty w stylu JAX-WS, które to przewalą na kod javowy bardzo często mający więcej nulli niż rzeczywistych wartości. Ponieważ artykuł nie jest o xmlach i klasach javy wypełnionych po brzegi annotacjami to zrobimy wygodny skok/uproszczenie i dla potrzeb edukacyjnych "ekstrakcję" zasymilujemy na zwykłej takiej javowej mapie.

Bezpieczny i silnie typowany kod Javy 9

Także oto wspomniana mapka na potrzeby edukacyjne :

Map<String, String> m1 = Map.of("X509SKI", "dupa");

No i teraz najważniejsze. Chcemy wydobyć ten element kiedy tak naprawdę nie wiemy czy on tam jest, robimy to tak :

 Optional<X509Data> result = Optional
                .ofNullable(m1.get("X509SKI")).map(X509Data.X509SKI::new)

No i fajnie ale to nie jedyna opcja w tej opcji - przecież możemy mieć "X509CLR" i teraz własnie wchodzi or

 Optional<X509Data> result = Optional
                .ofNullable(m1.get("X509SKI")).<X509Data>map(X509Data.X509SKI::new)
                .or(
                        () -> Optional.ofNullable(m1.get("X509CLR")).map(X509Data.X509CLR::new)
                )

  • Obserwacja nr1 : Java nie ma czegoś takiego jak "pass by name" w skutek czego trzeba to zasymulować przekazaniem Supplier stąd to "()->" na początku
  • Obserwacja nr2 : zauważcie to .<X509Data>map : jeszcze nie pokazałem jak te typy sa modelowane ale generalnie X509Data to nad-typ i niestety wykrywanie typów w Javie kuleje jeszcze na tyle, że trzeba mu tutaj pomóc i powiedzieć żeby trzymał się typu bazowego.

No i ostatecznie mamy :

Optional<X509Data> result = Optional
                .ofNullable(m1.get("X509SKI")).<X509Data>map(X509Data.X509SKI::new)
                .or(
                        () -> Optional.ofNullable(m1.get("X509CLR")).map(X509Data.X509CLR::new)
                ).or(
                        () -> Optional.ofNullable(m1.get("X509IssuerSerial"))
                        .map(data -> new X509Data.X509IssuerSerial(data, 69))
                );

Closed Algebraic Data Type

Wróćmy teraz do modelu danych. Mamy pewien abstrakcyjny typ z określonymi jego realizacjami X509SKI, X509CosTamInnego itd. Inne języki niż Java wspierają taki typ przy pomocy konstrukcji, która z jednej strony nie jest finalna w nad-klasie a z drugiej pozwala jedynie na zadeklarowanie jasno określonej listy pod typów. (więcej na ten temat : http://pawelwlodarski.blogspot.com/2017/02/closedclosed-principle.html) . W javie okazuję się jest też pewien "sposób" na takie rozwiązanie. Pod spodem zobacyzmy ograniczoną listę klasy wewnętrznych i klasę zewnętrzna z prywatnym konstruktorem -> czyli nic poza ta klasą nie stworzy nowej podklasy bo nie ma dostępu do konstruktora nadklasy - w teorii tak to własnie powinno działać.

//Java sealed type dla ubogich
class X509Data {

    private X509Data() {
    }

    final static class X509SKI extends X509Data {
        final String value;

        X509SKI(String value) {
            this.value = value;
        }
    }

    final static class X509CLR extends X509Data {
        final String value;

        X509CLR(String value) {
            this.value = value;
        }
    }

    final static class X509IssuerSerial extends X509Data {
        final String name;
        final Integer number;

        X509IssuerSerial(String name, Integer number) {
            this.name = name;
            this.number = number;
        }
    }
}

Wygodniej w Kotlinie

Ogólnie fajnie, że java się rozwija i stara się nadganiać ale niektóre rzeczy wciąż wyglądają jak wiadro z gównem. Toteż uwagę mą przykuwa coraz popularniejszy Kotlin, który pod kontem ekspresji jest dla mnie gdzieś pomiędzy Javą a Scalą - i co dobre wydaje się być jeszcze przed granicą gdzie nie udostępnia potężnych ale i niebezpiecznych mechanizmów, którymi dzieci w korporacjach robią sobie krzywdę. A co najważniejsze dla naszego przykładu - ma natywne wsparcie dla rekordów danych oraz typów algebraicznych

sealed class X509Data
data class X509SKI(val value:String):X509Data()
data class X509CLR(val value:String):X509Data()
data class X509IssuerSerial(val name:String,val number:Int):X509Data()

I mając rozwiązanie na największy ból obecnej javy czyli definicje małych klas rozjebane na dwa ekrany możemy przejść do naszego głównego przykładu

val m1=mapOf("X509CLR" to "dupa")

val result:Optional<X509Data> = Optional
            .ofNullable(m1["X509SKI"]).map<X509Data>(::X509SKI)
            .or{ Optional.ofNullable(m1["X509CLR"]).map(::X509CLR)}
            .or{ Optional.ofNullable(m1["X509IssuerSerial"]).map { data -> X509IssuerSerial(data,69) } }

Okazuje się, że Kotlin bardzo dobrze radzi sobie z konwersją pomiędzy swoimi lambdami a Javowymi SAM types. Trzeba tylko przywyknąć do tych klamerek takich i będzie ok.

Edukacyjna rola Scali

Operator Optional.or, który jest nowością w Javie 9 był od dawna dostępny w scali jako Option.orElse. Poniżej jego definicja z charakterystyczną strzałką "=>" przed argumentem. Ten zapis oznacza "call by name" czyli argument jest zwyczajnie ewaluowany leniwie. Ponieważ w Javie nie ma czegoś takiego to zwyczajnie trzeba było się tam spodziewać Supplier. No i tak znając jeden język można z marszu przejść do używania "nowinek" w innym języku.
  @inline final def orElse[B >: A](alternative: => Option[B]): Option[B] =
    if (isEmpty) alternative else this

Podsumowanie

Dzisiaj połączyliśmy technologię w miarę nowoczesną - Java 9 - która mimo wszystko trochę jest w plecy za resztą świata - z przykładem technologii tak starej, że pewnie wasz dziadek używał kiedy w dyliżansie przemierzał dziki zachód. To co chciałbym byście wynieśli to jednak ilustracja zastosowania operatora na poziomie typów Optional. Bo to już nie takie pseudo "ifowanie" w stylu

Zaczytałem se jakiegoś syfa
użyje ifPresent czy getOrElse
i z Optionala mam kurwa zwykłego IFa
To co pojawia się w przypadku Optional.or to - algebra - tak samo jak algebra boolea dla true|false . Java zmierza w dobrym kierunku (byleby jakiemu geniuszowi nie przyszło do głowy jebnąć na Optional jakiejś annotacji @orrable która w runtime obrzyga całe typesafety, nie , prosze nie, nie , poprostu nie)