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.

Brak komentarzy:

Prześlij komentarz