Kotlin + Akka Typed

pawelwlodarski.blogspot.com 6 lat temu

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 iż 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 adekwatnie 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() {

    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 zwykle komendy idą do companion object bądź do innego object jeżeli 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 = Actor.mutable { ctx ->
Greeter1() }
}

Nie rozmieniłem jeszcze dokładnie dlaczego ostatnie nie działa ale trzeba zaznaczyć, iż ostatni przykład byłby najbardziej intuicyjny dla programisty scali dlatego tez od niego zaczynałem. Generalnie w Kotlinie mam odczucie, iż 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 : Actor.MutableBehavior() {

    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 adekwatnym 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żeli 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() {

    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 choćby 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, iż Kotlin celuje w bycie lepszą Javą i to się może udać. Część druga niedługo a cześć ósma jak będzie 100 000 lajków.

Idź do oryginalnego materiału