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 TUTORIALobject 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.