Fajny nowy acz niepozorny operator z Javy 9

pawelwlodarski.blogspot.com 7 lat temu

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, iż ani nie wiadomo czy jakaś wartość będzie przekazana a jeżeli 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, iż 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. istotny jest ten choice który sprawia, iż 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 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 .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, iż 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, iż 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ę, iż 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, iż 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)

Idź do oryginalnego materiału