niedziela, 4 lutego 2018

Kotlin + Akka Typed

Ale po co?

Tak dla ćwiczeń.

No ale po co?

O ile w "świecie scali" ten język wydaje się być ściśle powiązany z Akka to jednak w świecie korporacji gdzie management wykształcony na industrialnych praktykach wciąż widzi dodatnią korelację pomiędzy headcountem a powodzeniem projektu - no i własnie tam coś ta scala się tak nie przyjmuje w tych korporacjach. Jest duży opór. A że korpo to hajs to Akka tez ma API Javowe. Ale jak ktoś pisał w scali to to API Javowe boli strasznie. I tutaj pojawia się Kotlin jako kandydat na "lepszą Javę" - i to co mam nadzieję zobaczymy w artykule - Kod napisany w Kotlinie używającym API dla Javy w praktyce wygląda bardzo podobnie do scali używającej API dla Scali!

No a do tego będzie okazja przećwiczyć akka typed bo trochę mnie wkurwia to receive w aktorach, które łyka co wpadnie.

Ale na pewno o to chodzi?

No dobra. Za Kotlina teraz dostaje się więcej lajków. Zbiór fanbojów akki pokrywa się ze zbiorem fanbojów scali w dużym topniu a Kotlin to teraz mobilki i może jacyś fanboje springa także suma tych zbiorów może da 100 000 lajków. Jak będzie 100 000 lajków pod tym postem powstanie część ósma.

Oddziczanie Javy Kotlinem

Przykład w Scali skopiowany z tutoriala

Jako materiał użyjemy intro blog posta z blogu Akki do akka typed https://akka.io/blog/2017/05/05/typed-intro. Jest to ładne wprowadzenie gdzie mamy użyte dwa typy aktorów - jeden taki bardziej obiektowy ma sobie zmienną a drugi (chyba) bardziej funkcyjny generuje z zachowania nowe zachowanie.

Najpierw zobaczymy przyklejone kawałek Scali z dokumentacji a następnie analogiczny kawałek kotlina. Pod obiema próbkami kodu będzie opis/wyjaśnienie co właściwie trzeba było zrobić.


object Greeter1 {
  sealed trait Command
  case object Greet extends Command
  final case class WhoToGreet(who: String) extends Command

  val greeterBehavior: Behavior[Command] =
    Actor.mutable[Command](ctx => new Greeter1)
}

class Greeter1 extends Actor.MutableBehavior[Greeter1.Command] {
  import Greeter1._

  private var greeting = "hello"

  override def onMessage(msg: Command): Behavior[Command] = {
    msg match {
      case WhoToGreet(who) =>
        greeting = s"hello, $who"
      case Greet =>
        println(greeting)
    }
    this
  }
}

Analogiczny przykład z Kotlina

class Greeter1 : MutableBehaviorKT<Greeter1.Protocol.Command>() {

    private var greeting = "hello"

    override fun onMessage(msg: Command): Behavior<Command> {
        when (msg) {
            Command.Greet -> println(greeting)
            is Command.WhoToGreet -> greeting = "hello ${greeting}"
        }
        return this
    }

    companion object Protocol {
        sealed class Command {
            object Greet : Command()
            data class WhoToGreet(val who: String) : Command()
        }

        val greeterBehaviour: Behavior<Command> = Actor.mutable<Command> { ctx -> Greeter1() }
    }
}

Ok jest jedna zaślepka MutableBehaviorKT ale to tylko taki mały adapter by zutylizować(kocham to słowo) Kotlinow when w miejsce Javowych builder - zara wkleję. Jednak najpierw skoncentrujmy się na tym jak podejść do modelowania komend/protokołu/interfejsu aktorów. W scali zazwyczaj komendy idą do companion object bądź do innego object jeśli interfejs obejmuje kilku aktorów. W Kotlinie także możemy stworzyć companion object jednak istnieją pewne ograniczenia ograniczające miejsce wystąpienia do ciała klasy. Jednym się spodoba innym nie.

W każdy razie zerknijmy na poniższe sytuacje.

DZIAŁA:

companion object Protocol {
        sealed class Command {
            object Greet : Command()
            data class WhoToGreet(val who: String) : Command()
        }

        val greeterBehaviour: Behavior<Command> = Actor.mutable<Command> { ctx -> Greeter1() }
}
TEŻ DZIAŁA:
companion object Protocol {
        val greeterBehaviour: Behavior<Command> = Actor.mutable<Command> { ctx -> Greeter1() }
}

sealed class Command
object Greet : Command()
data class WhoToGreet(val who: String) : Command()
NIE DZIAŁA:
companion object Protocol {

   sealed class Command
   object Greet : Command()
   data class WhoToGreet(val who: String) : Command()

   val greeterBehaviour: Behavior<Command> = Actor.mutable<Command> { ctx -> Greeter1() }
}

Nie rozmieniłem jeszcze dokładnie dlaczego ostatnie nie działa ale trzeba zaznaczyć, że ostatni przykład byłby najbardziej intuicyjny dla programisty scali dlatego tez od niego zaczynałem. Generalnie w Kotlinie mam odczucie, że Companion Object został bardziej stworzony z myślą o metodach niż jako taki "całościowy moduł" dlatego też typy danych lepiej grupować na poziomie pakietu.

Co to jest to MutableBehaviorKT ?

A to sobie taki adapterek zrobiłem by nie używać tych "niby-że-mamy-w-javie-pattern-matching" builderów

abstract class MutableBehaviorKT<T> : Actor.MutableBehavior<T>() {

    abstract fun onMessage(msg: T): Behavior<T>

    override fun createReceive(): Actor.Receive<T> = object : Actor.Receive<T> {
        override fun receiveMessage(msg: T): Behavior<T> = onMessage(msg)
        override fun receiveSignal(msg: Signal?): Behavior<T> = this@MutableBehaviorKT
    }
}

I od razu kilka słów wyjaśnienia. To "object : CośTam" to syntax na deklarację anonimowej klasy w Kotlinie. Tutaj nie używam żadnych builderów tylko deleguję receiveMessage do metody, która we właściwym aktorze zaimplementowana być powinna. Po co jest to receiveSignal jeszcze nie wiem.

Jeszcze raz porównując logikę Scala-ScalaAPI & Kotlin-JavaAPI+adapter

SCALA:
 override def onMessage(msg: Command): Behavior[Command] = {
    msg match {
      case WhoToGreet(who) =>
        greeting = s"hello, $who"
      case Greet =>
        println(greeting)
    }
    this
  }
KOTLIN:
  override fun onMessage(msg: Command): Behavior<Command> {
        when (msg) {
            Command.Greet -> println(greeting)
            is Command.WhoToGreet -> greeting = "hello ${greeting}"
        }
        return this
    }

Immutable Aktor Numer Dwa

SCALA CODE COPIED FROM TUTORIAL
object Greeter2 {
  sealed trait Command
  case object Greet extends Command
  final case class WhoToGreet(who: String) extends Command

  val greeterBehavior: Behavior[Command] = greeterBehavior(currentGreeting = "hello")

  private def greeterBehavior(currentGreeting: String): Behavior[Command] =
    Actor.immutable[Command] { (ctx, msg) =>
      msg match {
        case WhoToGreet(who) =>
          greeterBehavior(s"hello, $who")
        case Greet =>
          println(currentGreeting)
          Actor.same
      }
    }
}
KOTLIN ALTERNATIVE FOR JAVA API:
sealed class CommandJava
data class WhoToGreet(val who: String) : CommandJava()
object Greet : CommandJava()


object Greeter2 {

    val greeterBehavior: Behavior<CommandJava> = greeterBehaviour(currentGreeting = "hello")

    private fun greeterBehaviour(currentGreeting: String): Behavior<CommandJava> =
            Actor.immutable<CommandJava> { _, msg ->
                when (msg) {
                    Greet -> {
                        println(currentGreeting)
                        Actor.same()
                    }
                    is WhoToGreet -> greeterBehaviour("hello ${msg.who}")

                }
            }
}

Tutaj zdecydowałem się wynieść komendy poza object z powodów opisywanych w poprzedniej cześć. Dodatkowo w przypadku Kotlina pojawia się jeden zestaw klamerek więcej gdyż when w przypadku gdy za strzałka występuje więcej niż jedna instrykcja wymaga własnie zamknięcia ich w klamerki podczas gdy scalowe match-case po prostu zgarnia wszystko od strzałki do strzałki. Ale tak poza tym wygląda bardzo podobnie.

We to uruchom

Tutaj też dosyć "scalowo" to wygląda.

 val root: Behavior<Nothing> = Actor.deferred<Nothing> { ctx ->
        val greeter: ActorRef<CommandJava> = ctx.spawn(Greeter2.greeterBehavior, "greeter")

        greeter send WhoToGreet("Java")
        greeter send Greet

        Actor.empty()
    }

ActorSystem.create(root, "HelloWorld")

Ale troszeczkę tutaj oszukuję. Generalnie w Scali wysłanie wiadomości do aktora wygląda tak : actor ! msg ale w Javie/Kotlinie nie można stworzyć takiej metody. Dodatkowo w Kotlinie jeśli chcemy wywołać metodę bez kropki i nawiasów to przy deklaracji trzeba to zaznaczyć operatorem infix. Oczywiście Javowe API tego nie ma i tam trzeba wołać klasycznie actor.tell(msg).

No i ja wtedy wchodzę cały na biało (z extend method):

infix fun <T> ActorRef<T>.send(cmd:T) = this.tell(cmd)

I to działa! Wołanie Kotlinem Javowego API scalowej biblioteki zakończyło się pełnym powodzeniem przyjaciele!

Wołanie Scali z Kotlina

A pojedźmy z eksperymentem trochę dalej i obadajmy jak wyjdzie wołanie bezpośrednio API scalowego z kotlina.

Pierwszy przykład wygląda bardzo podobnie do odpowiednika ze Scali. Nie ma żadnego buildera tylko zwykła metodka onMessage i obyło się bez adapterów.

class HelloScala1 : Actor.MutableBehavior<HelloScala1.Protocol.Command>() {

    private var greeting = "hello"

    override fun onMessage(msg: Command): Behavior<Command> {
        when (msg) {
            Command.ScalaGreet -> println(greeting)
            is Command.ScalaWhoToGreet -> greeting = "hello ${greeting}"
        }
        return this
    }

    companion object Protocol {
        sealed class Command {
            object ScalaGreet : Command()
            data class ScalaWhoToGreet(val who: String) : Command()
        }

        val greeterBehaviour: Behavior<Command> = Actor.mutable<Command> { ctx -> HelloScala1() }
    }
}

A teraz będzie coś fajnego

Ogólnie tutaj tez poszło gładko ale zerknijmy na jeden fragment :

  Actor.immutable { _, msg ->
                when (msg) {
                    is ScalaWhoToGreet ->
                        greeterBehavior("hello, ${msg.who}")
                    ScalaGreet -> {
                        println(currentGreeting)
                        Actor.same<Command>()
                    }

                }
  }

Czy widzicie coś tutaj ciekawego? No kurde... Przecież to jest API Scalowe a kotlinowa funkcja weszła tam jak w masło. Mowa o tym : " Actor.immutable[Command] { (ctx, msg) =>..." , oto co moim zdaniem się dzieje. Od Scali 2.12 Lambdy idą w natywny invokeDynamic toteż Kotlin pewnie widzi zwykłą lambdę z Javy8 - i nawet IDE podpowiada tam Function2. No i teraz poniewaz Kotlin ma filozofię "ma działać wygodnie z Java i chuj", więc lambdy w wywołaniach Javowego API są tłumaczone na SAM types i chyba tak to wyszło. Elegancko.

To we teraz to uruchom

 val root = Actor.deferred<Nothing> { ctx ->
        //scala default parameters not working in kotlin -> props
        val greeter: ActorRef<Command> = ctx.spawn(HelloScala2.greeterBehavior, "greeter", Props.empty())
        greeter.tell(ScalaWhoToGreet("ScalaExample"))
        greeter send ScalaGreet
        Actor.empty()
    }


//    `ActorSystem$`.`MODULE$`.apply()  //pure scala API with default parameters not recognised by Kotlin
    ActorSystem.create(root,"HelloWorld") // create is actually Java API

Tutja zaczęły się trochę schody bo Kotlin nie ma pojęcia o Scalowym cukrze składniowym także zapomnij o Object.apply i scalowych parametrach defaultowych. No i jak się uprzesz tykać konstrukcji nienaturalnych dla Javy to czeka cię coś w stylu : "`ActorSystem$`.`MODULE$`.apply()". Można to "owrapować" kotlinowym API ale ogólnie - aktor jest stworzony przez Scalowe API a ActorSystem przez Javowe i wszystko działa elegancko.

Wnioski

Hejty na bok ale jak twórcy Akki dodali Javowe API by przyśpieszyć adaptację tej biblioteki w korpo to moim zdaniem dobrze mieć Kotlina gdzieś na radarze bo to się czuje, że Kotlin celuje w bycie lepszą Javą i to się może udać. Część druga wkrótce a cześć ósma jak będzie 100 000 lajków.

niedziela, 21 stycznia 2018

Java9 Cleaner świetną ilustracją enkapsulacji

10 lat temu w Effective java mogliśmy przeczytać by nigdy nie używać finalize. Ja się zastosowałem i tylko raz niechcący w ferworze refaktoringu przezwałem jakąś metode na finalize i zaczęły dziać się śmieszne rzeczy. Latka minęły i świeżo na półeczki księgarni informatyka wyszło trzecie wydanie Effective java z apdejtem do Java 9. No i w tej najnowszej Javie (najnowszej jeszcze tylko 2 miesiące!) w rozdziale o finalize pojawia się nowy mechanizm java.lang.ref.Cleaner

Koniec końców Joshua Bloch napisał, że ten Cleaner tez zjebany ale ogólniej mniej i jest taki jeden kejs gdzie go bez strachu użyć można. No i tak się składa, że moim zdaniem ten przykład użycia jednocześnie niesie ze sobą ogromną wartość edukacyjną jak właściwie enkapsulować stan w klasie. A do tego możemy sobie zobaczyć jak inne języki na JVM poradzą sobie ze standardową sytuacją w Javie.

Przez chwile miałem wątpliwości czy aby temat będzie dla ogółu interesujący ale potem sobie przypomniałem, że to mój blog i będę tutaj pisał co mi się chce.

Dobra Enkapsulacja

Na poczatku wytniemy kawałeczki kodu by łatwiej było zauważyć wspomnianą enkapsulację i ogólnie pojęte ukrywanie informacji, które bardzo pomaga w utrzymaniu i rozbudowie systemu. Jeśli ktoś chce brzmieć bardziej Fancy to może używać terminu anglojęzycznego https://en.wikipedia.org/wiki/Information_hiding. Jeśli ktoś chce brzmieć bardziej biznesowo to może powiedzieć, że informejszyn hajding redukuje ołweral meintenance cost and reduces tajm to market for fiuczer rilises.

public class InstanceAroundResource implements AutoCloseable{
    //We will delegeta cleaning to Cleaner from Java 9
    private static final Cleaner cleaner=Cleaner.create();

    //This is definition of internal state, static -> so it has no ref to external instance
    //private - to better hide information
    private static class EncapsulatedResource implements Runnable{
       (...)
    }

    //no getters for those two fields, no strange hakiers annotations either
    private final EncapsulatedResource state;

    //this triggers cleaning procedure
    private final Cleaner.Cleanable cleanable;

    public InstanceAroundResource(String resourceId) {
        //notice that both instances are created inside constructor , no direct assignment, 
        //no information how resourceId is escapes outside 
        //compare this with stntaxt 'this.field = field' which is hiding information like in the sentence 
        // "think about number seven but don't tell what the number is"
        this.state = new EncapsulatedResource("[opened :"+resourceId+"]");
        this.cleanable = cleaner.register(this, state);
    }

    // here we are connecting Cleaner with Autocloseable from Java7
    @Override
    public void close() throws Exception {
        cleanable.clean();
    }
}

I teraz tak, po pierwsze primo od javy 9 mamy import do dyspozycji:

import java.lang.ref.Cleaner;

i idąc dalej :
  1. Zauważcie, że nic z tej klasy nie ucieka poza abstrakcyjnem resourceId przekazanym do konstruktora - to jest niejako kawałek publicznego interfejsu.
  2. Drugim kawałkiem publicznego interfejsu jest - no własnie interfejs - Autocloseable , czyli obietnicę posiadania metody/zachowania close
  3. Totalnie ukryliśmy przed użytkownikiem użycie Cleanera. Nie ma ani dziwnych ch** wie po co getterów setterów.
  4. Jak Bloch przykazał pola są finalne - przynajmniej pod katem referencji wnętrze jest niezmienne.
  5. Ukryliśmy przed światem zewnętrznym istnienie klasy EncapsulatedResource co daje nam pełne pole do refaktoringu tego kawałka kodu w przyszłości (i minimalizacji tajm to market)
  6. Jeszcze raz podkreslmy, że parametry konstruktora również nie zdradzają za wiele o bebechach.
  7. Sam Cleaner z tego co zrozumiałem jest lepszy od starej metody finalize pod tym katem, że delegując czyszczenie do Cleanera - który nasłuchuje (listenerek taki) - nie będzie problemu z wyjątkami przy sprzątaniu bo Cleaner ma pełną kontrolę nad tym watkiem/procedurą i ogarnia. Tak przynajmniej napisał autor i ja mu wierzę!

I cały kod

import java.lang.ref.Cleaner;

public class InstanceAroundResource implements AutoCloseable{
    private static final Cleaner cleaner=Cleaner.create();

    //static to not have reference to external instance
    private static class EncapsulatedResource implements Runnable{

        String handleToSystemResource; //don't need to be private because EncapsulatedResource is private

        EncapsulatedResource(String handleTosystemResource) {
            this.handleToSystemResource = handleTosystemResource;
        }

        @Override
        public void run() {
            System.out.println("Closing system resource by cleaner :"+handleToSystemResource);
            handleToSystemResource="CLOSED";
        }
    }

    private final EncapsulatedResource state;

    private final Cleaner.Cleanable cleanable;

    public InstanceAroundResource(String resourceId) {
        this.state = new EncapsulatedResource("[opened :"+resourceId+"]");
        this.cleanable = cleaner.register(this, state);
    }

    @Override
    public void close() throws Exception {
        System.out.println("First In Auto-Closable");
        cleanable.clean();
    }
}

Zerknijmy teraz na odpalenie tego :

        //1
        try(InstanceAroundResource r = new InstanceAroundResource("BLOG-POST")){
            System.out.println("Using resource1");
        }

        //2
        new InstanceAroundResource("UNHANDLED-RESOURCE");
        System.out.println("r2 left alone");
        //System.gc();

sytuacja nr 1 prosta bo na końcu zostanie wykonane close i mamy

Using resource1
First In Auto-Closable
Closing system resource by cleaner :[opened :BLOG-POST]
Co do drugiej sytuacji to podobnie jak u autora książki także i u mnie backup nie zadziałał bez sztucznego wywołania System.gc()
r2 left alone
Closing system resource by cleaner :[opened :UNHANDLED-RESOURCE]

A Java 10 ?

W Javie 10 zadziała już poniższe :

jshell> try(var r=new InstanceAroundResource("Java10")){
   ...> System.out.println("Doing something in java 10");
   ...> }
Doing something in java 10
First In Auto-Closable
Closing system resource by cleaner :[opened :Java10]

Czyli już nie trzeba deklarować nowego InstanceAroundResource z zaznaczeniem iż jest typu InstanceAroundResource. Mniej czytania, inżynier może przeznaczyć moc operacyjną muzgu na redukcję tajm to market.

Inne Języki

Kotlin

Kotlin ma coś takiego :

public inline fun <T : AutoCloseable?, R> T.use(block: (T) -> R): R {
    var exception: Throwable? = null
    try {
        return block(this)
    } catch (e: Throwable) {
        exception = e
        throw e
    } finally {
        this.closeFinally(exception)
    }
}

Co oznacza, że mechanizmem "extends method" dorzuci metodę use do wszystkiego co rozszerza AutoCloseable. Także zadziała to i z naszym - napisanym w Javie - InstanceAroundResource.

val r=InstanceAroundResource("KOTLIN-EXAMPLE")

r.use { r: InstanceAroundResource ->
        println("Doing something in kotlin")
}

I to w sumie w niektórych kręgach półświatka programistycznego można nazwać metodą/funkcją wyższego rzędu. Wynik będzie analogiczny z przykładem Javowym :
Using resource1
First In Auto-Closable
Closing system resource by cleaner :[opened :BLOG-POST]

No i analogie idzie dalej, jeśli chcemy by cleaner zadziałał jako backup trzeba ręcznie gc wywołać :

InstanceAroundResource("UNHANDLED-IN-KOTLIN")
println("r2 left alone")
System.gc()

A co z nullem

Ogarnianie nulli z javy to jeden z lepszych fiuczerów Kotlina, zerknijmy czy jebnie coś takiego :

val r3:InstanceAroundResource? = null
r3.use { r: InstanceAroundResource? -> println("jebnie?") } //nie jebnie

No i po pierwsze nie da się łatwo oszukać kompilatora, że null jest typu InstanceAroundResource (ten taki znak zapytania na końcu) a po drugie to nie jebnie. Jak zerkniesz jeszcze raz na implementacje "use" to zobaczysz , ze w finally nie ma "close" a jest " this.closeFinally(exception)". I to wygląda mniej więcej tak :
internal fun AutoCloseable?.closeFinally(cause: Throwable?) = when {
    this == null -> {}
    cause == null -> close()
    else ->
        try {
            close()
        } catch (closeException: Throwable) {
            cause.addSuppressed(closeException)
        }
}

Czyli takie zamknięcie-otwarcie RISORSA nie jest rzeczą prostą. I całe szczęście, że Java i Kotlin mają gotowce na to w standardowej bibliotece. I tutaj też zwróć uwagę, że Java natywnie na poziomie języka a kotlin zaimplementowane przy pomocy bardziej abstrakcyjnego natywnego mechanizmu - "rozszerzalnych metod". Zaraz zobaczymy, ze to może iść jeszcze dalej.

Scala

I teraz tak : wiem, że google jakoś segreguje wyniki wyszukiwania na podstawie ciasteczek i mogą być dla każdego unikalne ale jak wpisałem w googla własnie "scala try with resources " to kurcze same tutoriale wyskakują jak samemu napisać a nie byłem w stanie jakiejś natywnej konstrukcji odnaleźć.

jak ktoś zna to niech da namiar bo inaczej ryzykujemy w każdym projeckie "not inwented here" z pytaniem "a w której paczce mamy zaimplementowany loan-pattern, no wiesz ten try z javy?". Jest biblioteczka https://github.com/jsuereth/scala-arm ale tez jakos nie jest dobrze na googlu spozycjonowana a znalazłem ją bo już o niej kiedyś słyszałem. W każdym razie jak już biblioteczkę mamy to dalej kod wygląda bardzo podobnie:

val r=new InstanceAroundResource("SCALA-EXAMPLE")

val mr: ManagedResource[InstanceAroundResource] = managed(r)

mr.foreach{r=>
        println("Doing something in scala")
}

new InstanceAroundResource("UNHANDLED-IN-SCALA")
println("r2 left alone")
System.gc()
Doing something in scala
First In Auto-Closable
Closing system resource by cleaner :[opened :SCALA-EXAMPLE]
r2 left alone
Closing system resource by cleaner :[opened :UNHANDLED-IN-SCALA]

Poziomy abstrakcji mechanizmów językowych

Obczajcie to :

  • Java ma try-with-resources
  • Kotlin ma extension method przy pomocy, którego można zaimplementować try-with-resources i inne rozszerzenia
  • Scala ma implicity przy pomocy których można zaimplementować extension method i inne meta-rozszerzenia przy pomocy których można zaimplementować try-with-resources i inne rozszerzenia
No tylko, że im bardziej ogólny mechanizm tym trudniej go komuś wytłumaczyć i dłuższa edukacja - try-with-resources jest banalne w użyciu a teraz porównaj to z ogarnianiem implicit class i jakichś optymalizacji jak rozszerzanie AnyVal. Także coś za coś

Linki

sobota, 6 stycznia 2018

Nauka Mechanizmów Programowania

... a nie tylko konkretnych języków. Taki oto temat dzisiejszego wypracowania. Można by rzec, że mechanizmów starych ale subiektywnie nowych dla ludzi, którzy siedzą w Javie. Przed Javą 8 warto było uczyć się mechanizmów z innych języków na JVM by być przygotowanym na nadejście funkcji. Teraz może warto pochylić się nad innymi elementami, których w Javie nie uświadczysz (albo napotkasz w formie ubogiej) jak 'declaration site variance' , wykrywanie typów czy modelowanie 'unii' przy pomocy słówka 'sealed'.

Narzędzie edukacyjne - by zmotywować uczestników - musi nosić jakieś znamiona przydatności na rynku lokalnym bo spotkania wokół Haskella czy bardziej zaawansowanego wykorzystania FP w Scali zainteresowały jedynie ograniczone grono. Scala jest fajna i bogata w przeróżne mechanizmy ale trudno zmotywować ludzi do jej nauki kiedy nie ma szans zrobić nań pieniądza. I tu pojawia się coś pomiędzy Scalą i Java czyli Kotlin.

Taka ma teoria, że ludzie z JetBrains pracując latami nad wsparciem scali nauczyli się dobrze, które ficzery łatwo wspierać w IDE a co lepiej usunąć. I tak w sumie jak ktoś przejdzie od Javy do Kotlina to z Kotlina dużo bliżej do Scali (jakby ktoś chciał). Język jest 'wystarczająco przyjemny' w porównaniu z Javą i ma wiele wygodnych mechanizmów, które występują w scali a do tego to już nie tylko Android ale różne narzędzia i inne Springi zaczynają go wspierać.

No to po kolei...

Plan Nauki

Na razie wbite są dwa meetupy

  1. Wprowadzenie do FP i OOP - https://www.meetup.com/Java-User-Group-Lodz/events/246218922/. To wprowadzenie należy rozumieć jako poznanie składni aniżeli jakiejś abstrakcyjnej teorii. Jak Kotlin się kompiluje do Javy, jak rozmawia z Javą a przede wszystkim o ile mniej pracy trzeba w tym języku włożyć w zadeklarowanie prostej klasy czy funkcji.
  2. System typów - https://www.meetup.com/Java-User-Group-Lodz/events/246378280/ - Kolejna rzecz trochę podobna do Scali czyli Niedziurawy system typów z Any na górze i Nothing na dole - do tego dochodzą interesujące Unie Typu i Nulla , które w końcu mogą rozwiązać problem porozrzucanych nullpointerexceptionów po kodzie

I co dalej :

  • Dużo ciekawszy mechanizm generyków, declaration site variance,covariance,contravariance i konsekwencje, o których pisałem rok temu : kowariantne konsekwencje
  • Kompozycja funkcji o niebo wygodniejsza ze względu na obecność prostych typów funkcji jak Int -> Int zamiast Function<Int,Int>, currying i takie tam podobne
  • Pattern matching, funkcje częściowe i całkowite i dlaczego to ważne. No i ADT czyli Algebraiczne typy Danych
  • Tutaj może wejdzie bardziej zaawansowane FP z bibliotekami https://github.com/MarioAriasC/funKTionale oraz Kategory przemianowane na Arrow
  • No i w międzyczasie może porobimy jakieś Code Retreat czy coding kata czy jakieś ćwiczenia w ten deseń

Także sytuacja będzie się rozwijać i jak ktoś jest zainteresowany niech śledzi meetupa.

W tym miejscu artykułów zazwyczaj występuje zakończenie także i tutaj nie może go zabraknąć.

niedziela, 12 listopada 2017

Functional Calisthenics z Kotlinem na GDCR 2017

Code Retreat - a zwłaszcza GLOBAL - to doskonała okazja do nauki programowania, zerwania z nieskutecznymi nawykami i odnalezienie nowych podejść do rozwiązywania problemów. A, że to jest to spotkanie cykliczne to akurat w tym miejscu wykorzystam starą metodę DRY i a TY będziesz dispatcherem/dispatcherką wywołania. Także wybierz sobie wstęp z jednego z poniższych artykułów.

  1. Motywacja do GDCR 2013
  2. Wnioski po GDCR 2013
  3. Kilka przykładów użycia mechanizmów FP ze scali do Game of Life
  4. Po GDCR 2014
  5. przed GDCR 2015 - tutaj w ogóle jakiś Haskell jest :D
  6. Po GDCR 2016 - przymiarki do funkcyjnego CR
  7. Wrażenia po GDCR 2016, którego nie było i tym razem trochę lepszy Haskell

Skoro już trochę historii za nami to wróćmy na chwilkę do ograniczeń. Jednym z popularniejszych na CR jest ograniczenie zwane object calisthenics, które jest nakierowane na poprawienie umiejętności pisania poprawnego obiektowego kodu. I od razu musimy sobie wyjaśnić jedną rzecz - jeśli z jakiegoś powodu nie podoba ci się programowanie obiektowe to masz jak najbardziej prawo je krytykować ale najpierw musisz zrozumieć o co w tym wszystkim chodzi. Jeśli dzień za dniem rzeźbisz klasy, które nazywają się ManagerCosTamImpl albo CosTamHelper to nie masz problemu z programowaniem obiektowym ale zwyczajnie piszesz nad wyraz złozony kod proceduralny w języku, który wymusza by ten kod proceduralny był w klasach - stąd te kolosy po 8000 linijek.

A wracając do ograniczeń to object calisthenics mają np. takie ćwiczenie "jedna kropka na linie kodu" i to ma pomóc pisać kod, który lepiej enkapsuluje dane w klasach bo trudniej o tzw. train wreck obj.get()A.get()B.setC(c). No i jako, że programowanie funkcyjne również zaczyna grać ważną role w twoim ulubionym języku to warto by podobne zasady wprowadzić dla FP.

W 2016 mieliśmy przerwę ale już w tym roku Code Retreat wraca na JUG Łódź. Spotkanie odbędzie się 18 listopada a zapisac można się tutaj -> Global Day of CR łódź Meetup. W tym roku będzie także zapewniona opieka nad dziećmi także można wpaść z rodziną.

No i by do tematu nauki podejść całościowo i rozszerzyć ćwiczenia o FP to tym razem najprawdopodobniej kilka ćwiczeń będzie zainspirowanych nowym zestawem functional calisthenics -> https://codurance.com/2017/10/12/functional-calisthenics/

No to jedziemy z koksem i obczajamy te zasady. Tym razem użyjemy języka, który zdobywa coraz większą popularność na JVM, ma dobre wsparcie w IDE bo robi go firma, która sama robi IDE a do tego jest wciąż bezpiecznie ubogi tak, ze korporacje nie powinny nim sobie zrobić krzywdy - mowa o Kotlinie.


Zasada 1 : Nazywaj wszystko i naucz się rozpoznawać wzorce w kodzie.

"All functions need to be named. Which means that you shouldn't use lambdas or anonymous functions. Also, name all your variables as per standard clean code rules."

Jeśli musisz dać czemuś nazwę to istnieje duża szansa, ze wiesz co ten fragment musi robić. No i teraz jeśli zamiast używać anonimowych funkcji zaczniesz je definiować i nazywać to może zauważysz szansę na parametryzacje i kompozycje. Ale aby to miało ręce i nogi najpierw musisz w ogóle zastosować podejście funkcyjne a tutaj moga pomóc bardziej fundamentalne ograniczenia jak np. brak zmiennych.

Co do implementacji to tutaj bardzo może się przydać nowy ficzer z kotlina 1.1 - aliasy typów.

//aliases since Kotlin 1.1
typealias Cell = Boolean
typealias Neighbours = Int

val LIVE_CELL:Cell = true
val DEAD_CELL:Cell = false


fun cellEvolution(c:Cell,n:Neighbours): Cell =
        when(c){
            LIVE_CELL -> if(n==2 || n==3) LIVE_CELL else DEAD_CELL
            DEAD_CELL -> if(n==3) LIVE_CELL else DEAD_CELL
        }

Powyżej pierwsza próba, w trakcie której próbowałem zadeklarować dwie stałe okreslające stan komórki DEAD oraz LIVE jakkolwiek kompilator (a przynajmniej plugin do Intellij) nie był w stanie wykryć iż wszystkie warunki w wywołaniu when zostały ogarnięte. Dlatego tez powstało drugie podejście - sealed class

sealed class Cell
object LiveCell :Cell()
object DeadCell :Cell()


fun cellEvolution(c:Cell,n:Neighbours): Cell =
        when(c){
            LiveCell -> if(n==2 || n==3) LiveCell else DeadCell
            DeadCell -> if(n==3) LiveCell else DeadCell
        }


Zasada 2 : Brak zmiennego stanu.

Dosłownie : "You shouldn't use any variable of any type that can mutate."

Najważniejsze w tym ćwiczeniu jest "zablokowanie" uczestnikom drogi w kierunku (nawet i poprawnego) programowania obiektowego gdyż w tym wypadku nie można owinąć zmiennego stanu instancją obiektu.

//THIS IS FORBIDDEN
class MutableCell(private var state:Boolean){
    fun evolve(n:Neighbours){
        //set new state here
    }
}

Pewne trudności moga sie pojawić kiedy będziemy chcieli zmienić stan planszy - no ale przecież zmiany nie są dozwolone!. Kotlin dosyć ładnie oddziela api służące do deklarowania i używania niemutowalnych kolekcji od mutowalnych odpowiedników. Aby transformować niemutowalne listy tworząc nowe niezależne wersje używamy funkcyjny wyższego rzędu jak map czy filter obecnych teraz nawet w Javie.

typealias Coordinates=Pair<Int,Int>
typealias CellPosition=Pair<Coordinates,Cell>
typealias Board = kotlin.collections.List<CellPosition>

val nextRound : (Board) -> Board = {board ->
    board.map { ??? }
}

No i zobacz czytelniku/czytelniczko jakim wygodnym pomysłem są aliasy w przykładzie powyżej.

Zasada 3 : Każdy argument na wejściu zwraca wynik

"There can not be an if without an else. Switches and pattern matching should always have all paths considered"

To jest dosyć ciekawa zasada bo dobrze oddaje naturę code retreat. Tak więc mamy proste założenie, którego uczestnicy mają się trzymać i obserwować wyniki. Jest to na początku znacznie prostsze aniżeli wchodzenie w rozważania teoretyczne o funkcjach częściowych. Każdy argument musi generować konkretny wynik - bez wyjątków - dosłownie!

Zasada 4 : Nie używaj zmiennych tymczasowych

Zasada 5 : Expressions, not statements

Te dwie zasady postanowiłem połączyć bo w moim odczuciu dotyczą podobnego tematu. Dodatkowo druga zostawiam po engliszu by nie popełnić błędów w tłumaczeniu.

"There shouldn't be any variable declared in the body of a function." oraz "All lines should be expressions. That's it, all lines should return a value."

Tutaj języki takie jak Kotlin czy Scala mogą pomóc bardzo, bardzo, bardzo bo tam wszystko zdaje się coś zwracać! A szczególnie if jest wygodny jak coś zwraca. W Javie zazwyczaj trzeba coś tam manipulować po bokach.

fun badEvolve(cell:Cell,neighbours:Int):Cell{
    var newCell:Cell?=null //val newCell:Cell would also work here but var shows problem better
    if(cell==LiveCell && neighbours in (2..3))  {
        newCell = LiveCell
    }
    else if(cell==LiveCell)  {
        newCell = DeadCell
    }
    else if(cell==DeadCell && neighbours==3) {
        newCell=LiveCell
    }
    else {
        newCell=DeadCell
    }

    return newCell
}

W Javie często deklarowane są zmienne tymczasowe (pamiętacie te czasy gdy 90% zmiennych w waszym godzinie nazywało się temp?) tylko po to by coś w nich na chwilę przypisać i od razu dać return. Wyjście? Niech if coś zwraca i niech to będzie wynik metody. No i w Kotlinie to dzieje sie naturalnie. W Javie niby jest tzw. "ternary operator" ale on chyba dlatego nazywa się ternary bo nie ma "if-else". Trochę peszek.

fun betterEvolve(cell:Cell,neighbours:Int):Cell{
    return if(cell==LiveCell && neighbours in (2..3)) LiveCell
    else if(cell==LiveCell) DeadCell
    else if(cell==DeadCell && neighbours==3) LiveCell
    else DeadCell
}

No i teraz najlepsze. Ponieważ jest to jedyna instrukcja funkcji to możemy wyrażenie uprościć jeszcze bardziej.

fun bestEvolve(cell:Cell,neighbours:Int):Cell =
        if(cell==LiveCell && neighbours in (2..3)) LiveCell
        else if(cell==LiveCell) DeadCell
        else if(cell==DeadCell && neighbours==3) LiveCell
        else DeadCell

Zasada 6 : Brak jawnej rekurencji

Przyznam, że tego punktu do nie rozumiem a to z faktu, że autorzy odnoszą się do jakichś konstrukcji z Clojure. Coś tam kiedyś czytałem o "loop/recur" ale nie będę udawał, że pamiętam. Tak czy inaczej w tym punkcie znajdziemy poradę by tę "jawną rekurencję" zastąpić map/reduce . No to tak będziemy starali się zrobić.

Zasada 7 : Generyczne bloki

"Try to use a much as possible generic types for your functions, outside of the boundaries of your application"

To jets dosyć interesujący i abstrakcyjny sam w sobie punk. Z jednej strony ile razy deklarowałeś listę kiedy kiedy Collecion lub Iterable w zupełności wystarczało? Z tym ograniczeniem będziesz musiał dokładnie uzasadnić czy typ, którego używasz nie jest zbyt konkretny co może związać ci ręce przy dalszych zmianach. Dodatkowo w Kotlinie ze względu na wykrywanie typów musisz się zastanowić czy pozostawić prace kompilatorowi, który pewnie znajdzie najbardziej konkretny typ czy też może zadecydować świadomie samemu.

Możesz zastosować podobne podejście do funkcji ale tutaj idzie to trochę w inną stronę. Czy obecna implementacja dla danego typu nie jest zbyt "wąska"? Klasycznym przykładem jest tutaj reduce które jest generalizacją dodawania, mnożenia i czego tam sobie zażyczymy.

fun  add(list:List<Int>) : Int =if(list.isEmpty()) 0 else list[0] + add(list.drop(1))
fun  multiply(list:List<Int>) : Int =if(list.isEmpty()) 1 else list[0] * multiply(list.drop(1))

fun <A> genericReduce(l:List<A>,neutral:A,reduction:(A,A)->A):A =
        if(l.isEmpty()) neutral else reduction(l[0], genericReduce(l.drop(1),neutral,reduction))

I zgodnie z oczekiwaniami

println(add(listOf(1,2,3,4,5)))
println(multiply(listOf(1,2,3,4,5)))

println(genericReduce(listOf(1,2,3,4,5),0,{a,b->a+b}))
println(genericReduce(listOf(1,2,3,4,5),1,{a,b->a*b}))

Zasada 8 : Efekty uboczne tylko na granicy systemu

"Side effects should only appear on the boundaries of your application. The guts of your application should have no side effects."

Tutaj po pierwsze musisz używać funkcji, które nie zmieniają żadnego stanu poza nimi żadnego println czy też modyfikowania mutowalnej tablicy zdefiniowanej poza funkcją. To ograniczenie może być jeszcze bardziej interesujące gdy założysz, że efekty na granicy systemu musza być reprezentowane przy pomocy odpowiednich typów.

W takich wypadkach możesz skorzystać z typu Optional dodanego do Javy8

typealias Coordinates=Pair<Int,Int>
typealias CellPosition=Pair<Coordinates,Cell>
typealias Board = kotlin.collections.List<CellPosition>

val initializeBoard:(Coordinates) -> Board = ...
val nextTurn: (Board) -> Board = ...


fun readInput():Optional<Coordinates> = ...

fun display(b:Board):Unit = ...

readInput().map(initializeBoard).map(nextTurn).ifPresent(::display)

W przykładzie powyżej możesz zobaczyć, że Kotlin całkiem wygodnie integruje Jawowe klasy. Optional służy tam do zasygnalizowania efektu ubocznego związanego z wprowadzaniem danych ze świata zewnętrznego - który jakby nie patrzyć jest trochę uboczny.

Bardziej zaawansowani uczestnicy mogą zechcieć skorzystać z również bardziej zaawansowanej biblioteki http://kategory.io/ i użyć IO Monad do wypisywania tekstu na konsolę http://kategory.io/docs/effects/io/ .

Zasada 9 : Nieskończone strumienie danych

W skrócie - nie możesz używać tablic ani kolekcji. Tam gdzie użyłbyś standardowej kolekcji musisz użyć strumieni/ nieskończonej sekwencji

Programowanie Funkcyjne kładzie ogromny nacisk na operowanie na wartościach bez mutowania stanu. Jeśli nie zmieniamy stanu i wartości są stałe to czy ważne jest kiedy operacje będą miały miejsce (stąd FP ułatwia operacje wielowątkowe)? No nie ma w związku z czym możemy pracować na tzw. leniwych kolekcjach które są niejako "opisane" ale nie istnieją puki nie trzeba.

Przykładowymi kandydatami do "strumieniowania" w grze życie są komórki - na z definicji nieskończonej planszy. Również gra sama w sobie jest nieskończoną kolekcją tur.

//here we are playing 10 turns
val init:Board = initializeBoard()
generateSequence(init,nextTurn).take(10).forEach(::display)

Ale zamiast 10 tur możemy chcieć grać do momentu kiedy na planszy mamy jakiekolwiek żywe komórki. Aby było bardziej interesująco przeskoczmy na chwile do Javy. Pierwszą rzeczą jaką chcemy sprawdzić to czy da się wykorzystać kod, ktory napisaliśmy w Kotlinie.

List<Pair<Pair<Integer, Integer>, Cell>> board = Life1Kt.initializeBoard();

Technicznie jest to możliwe ale praktycznie raczej nikomu nie będzie się chciało babrać z tak rozbudowanymi typami generycznymi. No dobrze to spróbujmy od początku :

Board init= ...;
Predicate<Board> hasLiveCells = ...;
UnaryOperator<Board> nextTurn = ...;
Consumer<Board> display=...;

//below iterate version possible from Java9
Stream.iterate(init,hasLiveCells,nextTurn).forEach(display);

//take while possible from Java9
Stream.iterate(init,nextTurn).takeWhile(hasLiveCells).forEach(display);

Zasada 10 : Tylko funkcje jedno parometrowe

"Each function should have a single parameter."

Według mnie to najważniejsze ograniczenie by dobrze zrozumieć siłę kompozycji funkcji. Gdy przekazujesz do metody całą paczkę parametrów możesz ją postrzegać jako wielki monolit. Lecz kiedy przesłanie kolejnych parametrów określa tak naprawdę innego rodzaju funkcje - wtedy musisz się zastanowić co z czego tworzysz. Dla przykładu funkcję findNeighbours możesz zaimplementować na dwa sposoby.

findNeighbours : (Coordinates) -> (Board) ->  Neighbours = ...

Czy widzisz różnicę? Czy lepiej mieć funkcje, która znajdzie sąsiadów dla przekazanych koordynat na z góry określonej planszy czy może wygodniej jest przekazywać planszę dla wcześniej orkeślonych koordynat?

Co do samego Kotlina to nie ma on tak wygodnego wsparcia dla curryingu jak scala (currying to własnie to, że przekazujesz parametr po parametrze) ale jak się trochę ogarnie składnie to też daje radę

val evolve : (Neighbours) -> (Cell) -> Cell = {ns->{c->c}}

Ewentualnie produkuj funkcję z metody

fun evolve2(ns:Neighbours) : (Cell) -> Cell = {c -> c}

Zasada X : Żadnych nulli

Ta zasada co prawda nie pojawia się w oryginalnym zestawie ale w programowaniu funkcyjnym null jest ... bez sensu. Kotlin ma ciekawe patenty na poziomie języka do radzenia sobie z nullami . Jeśli nie chcesz w nie wchodzić wtedy cóż, zawsze zostaje prawilne podejście z opakowywaniem każdego nullowego api jak map.get w Optional.

Podsumowanie

Podobnie jak chodząc na siłownię powtarzasz pewne ćwiczenia tak samo tutaj powtarzając cwiczenia nabierasz informatycznej siły. I analogicznie samo czytanie o ćwiczeniach niewiele ci da. Jeśli chcesz zwiększyć siłę programowania funkcyjnego ćwicz,ćwicz,ćwicz,ćwicz,ćwicz,ćwicz,ćwicz,ćwicz,ćwicz,ćwicz,ćwicz,ćwicz. Poszukaj spotkania code retreat w twojej okolicy lub podobnych warsztatów i nie opierdalaj się!

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)