czwartek, 7 marca 2019

Odmagicznianie

Patrząc na aplikację na jednym końcu jest framework (tu i ówdzie nazywany przez starych ludzi zrębem) a na drugim assembler, kod maszynowy, elektrony czy inne kwarki w zalezności od tego jak głęboko chcesz wchodzić. Im wyższy poziom tym - w teorii - powinno się szybciej pracować, gdyż ukrywana przed nami jest skomplikowana drobnica, ale - w praktyce - i tak prędzej czy później dostaniesz plaskacza w twarz od warstw poniżej (tutaj można klasycznie zaliczkować do artykułu o "cieknących abstrakcjach" z 1413 roku)

No i miała miejsce taka konkretna sytuacja. Robię warsztaty z nowego API Kotlina na Springu. To API jest fajne bo tam nie ma żadnych adnotacji (dlaczego to jest fajne to jeszcze o tym w naszym artykule będzie) i wykorzystując wiele z bardziej zaawansowanych kotlinowych mechanizmów pozwala zbudować konfigurację beanów,restów itd tylko i wyłącznie przy pomocy czystego kodu.

Warsztaty były z Kotlina. To API springowe dosyć dobrze napisane to myślę sobie - będzie to dobra ilustracja mechanizmów. Jednakże po zanurkowaniu w głąb kodu kilku osobom poleciała para z mózgu i tam przy okazji smoltoków padła taka koncepcja, ze w sumie może lepiej było zostać na poziomie wywołań i nie wchodzić w szczegóły.

Otóż wydaje mi się, że jednak nie byłoby lepiej. I to nie tylko dlatego, że taka przygoda ma to samo w sobie wartość edukacyjną w kontekście nauki Kotlina. Jest jeszcze jeden aspekt całej sprawy...

#10yearsczelendż - JDD 2009

Rok 2009 - czyli w świeci informatyki jakieś dwa wieki temu - był rokiem ciekawym. Wyobraź sobie, że świat jest w czymś czego młodzież IT rzucająca 10K na wejście jeszcze nie zna a co się nazywa "Kryzys Światowy".Java 6 jest szczytem nowoczesności. Youtube ma rozdzielczość 320p a zamiast fejsbuka jest nasza-klasa.

I właśnie wtedy miałem przyjemność uczestniczyć w konferencji JDD 2009 gdzie przyjechał pewien typ od JMS. No i tenże człowiek miał między innymiwykład o tym "jak NIE życ" czyli o antywzorcach w programowaniu. Lub coś takiego, coś w ten deseń. Było kilka ciekawych antywzorców ale nas interesuje ten jeden gdzie było coś o "magi" i pamiętam, że pokazał kawałek kodu z adnotacją @Transactional.


@Transactional
public void metoda(){
..
} 

To był Spring 3.coś i kontekst użycia magi polegał na tym, że ludzie wrzucali te adnotacje i mało kto wgłębiał się jak to działa. No i okazało się, ze w zależności od typu wyjątku jaki poleci - checked czy unchecked - działa to inaczej. Czasem będzie rollback a czasem nie (przynajmniej tak to działało A.D. 2009 )

Separacja i edukacja

Pomocne nam teraz będą dwa kawałki kodu.

Pierwszy przedstawia konfigurację RESTa adnotacjami. I widząc ten kawałek kodu w zasadzie trudno jest stwierdzić co się stanie. Trudno jest stwierdzić kiedy to się stanie.Trudno odpowiedzieć na pytanie czy jakiś wrapper będzie generowany w trakcie kompilacji, a może coś będzie czytać przez refleksję te adnotacje i jakaś bliżej nieokreślona logika pojawi się naokoło naszej klasy?

Dokładne przestudiowanie dokumentacji jest potrzebne - co samo w sobie nie musi być złe - ale to wymaga mocy skupienia aby zadość uczynić warunkowi "dokładne". A jak życie pokazuje dokumentacja może mieć nieścisłości, może mieć braki i dziury. Być może jest jakiś specjalny plugin do IDE, który powiąże adnotacje z miejscem wywołania i jakoś zbadać ten kod - ale raz, że nie wnikałem czy w ogóle coś takiego istnieje - a dwa, że uzależnia nas to od kolejnej rzeczy, kolejnego narzędzia.

  1. @RestController
  2. @RequestMapping("/foos")
  3. class FooController {
  4.  
  5. @Autowired
  6. private IFooService service;
  7.  
  8. @GetMapping
  9. public List<Foo> findAll() {
  10. return service.findAll();
  11. }
  12.  
  13. @GetMapping(value = "/{id}")
  14. public Foo findOne(@PathVariable("id") Long id) {
  15. return RestPreconditions.checkFound( service.findOne( id ));
  16. }

Do tego dochodzi jeszcze jeden ważny aspekt czyli - "kiedy tak naprawdę dowiem się, że coś zrobiłem źle i ile czasu zmarnuje aby rozpocząć naprawę buga". Kompilacja następuje (czasem dużo) wcześniej niż testy/odpalenie aplikacji - także jeśli czas to pieniądz to jest widoczna oszczędność pieniędzy. I znowu mogą pomóc jakieś pluginy do IDE tylko,że wiecie - naprawdę te pluginy to rozwiązanie sztucznego problemu, który sami sobie zrobiliśmy...

Teraz patrz na to:

  1. fun helloRouterFunction(): RouterFunction<ServerResponse> {
  2. //This one uses official kotlin DSL for building Routerfunction and Handlerfunctions
  3. return router {
  4. GET("/hello") { _ ->
  5. //and below is HandlerFunction
  6. ok().body(just("Hello World!"), String::class.java)
  7. }
  8. }
  9. }

Ok, jest to jakaś konstrukcja, której działania muszę się domyśla. Dokumentacja może być a może jej nie być. A Może mam jakieś specyficzne scenariusze, których nie ma w dokumentacyjnych hello worldach? Tutaj mam opcję! To zwykły kod. Można pobrać źródła , ctrl+b i juz patrzę co to za router

  1. fun router(routes: RouterFunctionDsl.() -> Unit) =
  2. RouterFunctionDsl().apply(routes).router()

Jest to prosta linijka kodu - prosta kiedy znasz kotlina, ale po to robię warsztaty - i jak już tego kotlina się umie czytać to to jest bardzo oczywiste co tutaj się dzieje. Jest taki builderek nazwany tutaj DSLem , do którego wpakujemy ustawienia routera. Na końcu jak to w builderach bywa jest wywołanie metody build , która tutaj nazywa się router

I teraz taka zagadka. Dlaczego mogę sobie napisać GET w środku routera ale już poniższe się nie kompiluje?

  1. GET("/hello") { _ ->
  2. //and below is HandlerFunction
  3. // Handler function is explained in IntroSpring2
  4. //and Reactor api in IntroSpring2 and MonoDemo
  5. ok().body(just("Hello World!"), String::class.java)
  6. }
  7. return router {
  8. //for explanation how GET is build take a look at IntroSpring2
  9. }

By odmagicznić sytuację zwyczajnie włażę w implementację tej metody i widzę, ze jest to metoda instancji wcześniej widzianego buildera, która modyfikuje stan tejże instancji - NO RACZEJ ma to sens by wołać ja w kontekście jakiegoś obiektu, EJ HELOŁ!

  1. /**
  2. * Route to the given handler function if the given request predicate applies.
  3. * @see RouterFunctions.route
  4. */
  5. fun GET(pattern: String, f: (ServerRequest) -> Mono<ServerResponse>) {
  6. routes += RouterFunctions.route(RequestPredicates.GET(pattern), HandlerFunction { f(it) })
  7. }

Wędka zamiast adnotacji

No i morał z tego taki, że im to wszystko, całe te mechanizmy są mniej przekombinowane tym naprawdę łatwiej rozkminić co się dzieje bo dokumentacji czasami jest jej jak na lekarstwo. Np. to Nowe API kotlinowe w springu to na jakimś blogu znalazłem opisane a dalej to sam już drążyłem i wynikiem sa materiały na gitbooku :

No i też te całe adnotacje interpretowane w runtime to trochę taki PHP napisany w Javie, macie dwa poziomy języka. To jest złe.

Warsztat na segfault university

Ten temat w formie 4-wymiarowego warsztatu poruszę na Segfault University w gdańsku w następnym tygodniu. Zapisywać można się chyba gdzieś tutaj : http://segfault.events/sites/gdansk2019/speakers/pawel-wlodarski/

sobota, 19 stycznia 2019

Kotlin plus Spring minus Adnotacje

Spring 5 dodał nowy - bardzo wygodny - sposób konfiguracji naszych przy użyciu prostych funkcji STOP Owo podejście nie wymaga stosowania adnotacji ani manipulacji bajtkodem STOP Co więcej Spring dostarcza natywne wsparcie dla Kotlina wykorzystując jego specyficzne mechanizmy, które znacznie upraszczają kod STOP A ponieważ nie jednego flejma o technologiach widziałem to we filozofie brnąć nie będę STOP A Tutaj jak ktoś chce są opisane problemy z adnotacjami The case against annotations STOP

Skoro o kodzie mowa

Rozpoczynając prezentację - dla potrzeb naszego przykładu edukacyjnego stworzymy następująca kolekcję typów, która to kolekcja pozwoli nam zobrazować jak wygląda konfiguracja z użyciem Kotlinowego DSL-a. Jakies interfejsy, kilka 'binów' i działamy :

interface I1
interface I2
class Bean1 : I1
class Bean2(dependency:I1) : I2
class Bean3(dependency:I2)

Aby użyć DSLka wystarczy jeden import :

import org.springframework.context.support.beans

No i już możemy pracować z bardzo - w moim odczuciu - przyjemnym kodem. I w moim osobistym odczuciu to naprawdę jest dosyć czytelna konfiguracja, która jednocześnie jest silnie typowana, czyli kompilator pilnuje nam pleców ,wyłapuje wszelkie babole , troszczy się itd:

  1. beans {
  2. bean<Bean1>()
  3. bean{
  4. val i1:I1=ref<Bean1>()
  5. Bean2(i1)
  6. }
  7. bean("Bean3"){
  8. val i2:I2=ref<Bean2>()
  9. Bean3(i2)
  10. }
  11. }

Powyższy fragment jest standardowym kodem Kotlina - nie ma tam żadnej magii a jedynie wykorzystanie natywnych mechanizmów języka. Deklarujemy nasze trzy beany pobierając zależności poprzez wywołanie metody. Wszystko jest kodem. Wszystko jest w jednym miejscu. Można to ogarnąć łatwo. Chaosu nie ma.

A jeśli nie do końca jeszcze wiadomo to, by lepiej zrozumieć sytuację zerknijmy w głąb metod. Mamy tego MR. beans i wygląda to nawet przyjaźnie ale tenże argument init jakis taki dziwny. Jakieś nawiasy po kropce. Co... co się tutaj stanęło?

  1. fun beans(init: BeanDefinitionDsl.() -> Unit): BeanDefinitionDsl {
  2. val beans = BeanDefinitionDsl()
  3. beans.init()
  4. return beans
  5. }

Otóż cała zabawa z tym DSLem jest możliwa dzięki zapisowi “init: BeanDefinitionDsl.() -> Unit” , który definiuje BeanDefinitionDsl jako odbiorcę wywołań w lambdzie (z engielska 'receiver'). Czyli dla przykładu we fragmencie

  1. beans {
  2. bean<Bean1>()
  3. ...
  4. }

Mamy 'implicit this' (dokładnie tak!) i tak naprawdę, wywołujemy

  1. beans { dsl ->
  2. dsl.bean<Bean1>()
  3. ...
  4. }

Teraz pojedynczy 'bean'. Tutaj pojawiają się dziwne nowe słowa:

  1. inline fun <reified T : Any> bean(....){....}

W tej metodzie dzieje się trochę więcej. Generalnie para słów kluczowych “inline” i “reified” w sprytny sposób zachowa informacje o typie z generyka w 'Runtime', dzięki czemu możliwe będzie utworzenie nowej instancji klasy Bean1 podanej w generyku. Podobna mechanika kryje się za metodą “ref”, która zwraca nam referencję do już zadeklarowanych beanów.

  1. inline fun <reified T : Any> ref(name: String? = null) : T =
  2. when (name) {
  3. null -> context.getBean(T::class.java)
  4. else -> context.getBean(name, T::class.java)
  5. }

Zauważ drogi czytelniku/czytelniczko iż zapis “T::class.java” robi coś niesamowitego i wyciąga klasę z generyka. Magia, po prostu magia….

Chociaż nie. Nie magia a nauka.

Inicjalizacja

Caly mechanizm "beans" to tak naprawdę zwykły wzorzec builder i tak jak na klasycznym buiderze wołaliśmy “build()” lub coś podobnego na sam koniec by zapieczętować budowę - tak tutaj przekazujemy do budowy springowy kontekst. Trochę to wygląda na cykliczną zależność ale w zasadzie nie zauważyłem jeszcze z tym problemów.

  1. val ourInit: BeanDefinitionDsl =beans {...}
  2. GenericApplicationContext {
  3. ourInit.initialize(this)
  4. refresh()
  5. }

I kontekst springowy zainicjalizowany naszymi beanami. Żadnych adnotacji nie trzeba, żadnego CGLiba i majstrowania przy bajtkodzie też nie trzeba!!! Ale to nie koniec. Kotlin i Spring 5 dają nam jeszcze więcej udogodnień bo teraz
skupcie się weźta
wystawimy resta

konfiguracja REST - uważaj na przykłady Javy!

Przede wszystkim czytając tutoriale do nowego mechanizmu - jak na przykład ten https://spring.io/blog/2016/09/22/new-in-spring-5-functional-web-framework - uważaj na użytą składnie Javy, gdyż używając w identyczny sposób Kotlina możesz napotkać na pewne trudności. Otóż API dla Javy zaleca użycie funkcji “route”, która jest zdefiniowana w sposób następujący

  1. public static <T extends ServerResponse> RouterFunction<T> route(
  2. RequestPredicate predicate, HandlerFunction<T> handlerFunction) {
  3.  
  4. return new DefaultRouterFunction<>(predicate, handlerFunction);
  5. }
  6.  

Gdzie "HandlerFunction" to prosty interfejs funkcyjny:

  1. @FunctionalInterface
  2. public interface HandlerFunction<T extends ServerResponse> {
  3. Mono<T> handle(ServerRequest request);
  4. }

Używając tego API możemy wygodnie wpisać sobie lambdę tam gdzie oczekiwany jest HandlerFunction a to dlatego, iż Java z automatu konwertuje ową lambdę na tzw. SAM czyli klasę abstrakcyjną/interfejs z jedną metodą. Dzięki temu możemy sobie napisać route(GET(“/”),r->...) . Niestety w Kotlinie przy konwersji lambdy na typ Javy jest potrzebna dodatkowa podpowiedź dla kompilatora:

  1. //route(GET("/test"), { r -> ok().body(Mono.just("response")) }) <- to nie pyknie
  2.  
  3. //poniżej podajemy podpowiedź, że ta lambda to HandlerFunction i działa
  4. route(GET("/test"), HandlerFunction { r -> ok().body(Mono.just("response")) })

Dlatego też warto używać dedykowany DSL -wiecie taki natywny ze springa a nie żadny na boku robiony - napisany specjalnie dla Kotlina. Konstrukcja tam użyta jest podobna do tego co widzieliśmy w poprzednim odcinku dla definicji Beanów - czyli dla przypomnienia:

  1. fun beans(init: BeanDefinitionDsl.() -> Unit): BeanDefinitionDsl {
  2. val beans = BeanDefinitionDsl()
  3. beans.init()
  4. return beans
  5. }

W przypadku definiowania “routingu” wygląda to bardzo ale to bardzo podobnie :

  1. fun router(routes: RouterFunctionDsl.() -> Unit) = RouterFunctionDsl().apply(routes).router()
  2.  
  3. router {
  4. GET("/hello") { _ ->
  5. ok().body(just("Hello World!"), String::class.java)
  6. }
  7. }

Jak możecie chyba zgadnąć GET to nic innego jak wywołanie metody na naszym DSLu i aby było jawne, jasne i klarowne co tam się dzieje to dzieję coś takiego :

  1. router {
  2. val dsl:RouterFunctionDsl = this //<- o tutaj cała magia
  3. dsl.GET("/hello") { _ ->
  4. //and below is HandlerFunction
  5. ok().body(just("Hello World!"), String::class.java)
  6. }
  7. }

W powyższym przykładzie GET jest po prostu wygodnym opakowaniem na to API dla Javy, o którym mówiliśmy na samym początku :

  1. fun GET(pattern: String, f: (ServerRequest) -> Mono<ServerResponse>) {
  2. routes += RouterFunctions.route(RequestPredicates.GET(pattern),
  3. HandlerFunction { f(it) })
  4. }
  5.  
  6.  

I problem “bojlerplejtu” rozwiązany.

Mono

Zwróć uwagę na sygnaturę funkcji przekazanej do Handlera :

f: (ServerRequest) -> Mono

Czym jest to Mono i jak wpływa na przetwarzanie requestu? O tym w innym odcinku...

poniedziałek, 4 czerwca 2018

Domenowo ale bez alokacji

Ten artykuł miał być początkowo wzmianką o ciekawym - acz niszowym mechanizmie - który umożliwia stosowanie pattern matchingu dla prostych typów bez alokacji niepotrzebnych obiektów na stercie. Jednakże w trakcie pisania i zabawy z javap -c ... okazało się, że tak w sumie to ja mam niekompletne pojęcie co się w wielu miejscach wyplutego z programu scali bytekodu JVM dzieje. Stąd też będzie to bardziej ogólny wpis o tym co produkuje kompilator i kiedy płacimy - a kiedy nie - dodatkową alokacją pamięci.

"Name based extractors"

Czasem dobrze zajrzeć do obcego kodu (ale tylko wtedy gdy pisał go ktoś faktycznie znający się na rzeczy) by odnaleźć warte przestudiowania konstrukcje. I taka konstrukcja pojawia się w Akka typed. Wertując bebechy Akki typowanej natrafimy na poniższe :

/**
 * INTERNAL API
 * Represents optional values similar to `scala.Option`, but
 * as a value class to avoid allocations.
 *
 * Note that it can be used in pattern matching without allocations
 * because it has name based extractor using methods `isEmpty` and `get`.
 * See https://hseeberger.wordpress.com/2013/10/04/name-based-extractors-in-scala-2-11/
 */
private[akka] final class OptionVal[+A >: Null](val x: A) extends AnyVal {

To jest bardzo interesujące. Zawsze wydawało mi się ,iż AnyVal i PatternMatching wzajemnie się wykluczają gdyż stoi jak wał w oficjalnej dokumentacji :


A value class is actually instantiated when:
  1. a value class is treated as another type.
  2. a value class is assigned to an array.
  3. doing runtime type tests, such as pattern matching.

Wrócimy do tego przykładu ale najpierw krótka wycieczka po alokacji obiektów w scali by lepiej zrozumieć co się dzieje gdy do pracy ruszy kompilator. I tutaj na potrzeby edukacji załóżmy, że chcemy mieć taki domenowy typ dla Miesiąca. Będzie bardziej domenowo i biznesowo. Miesiąc jest podzbiorem inta gdzie kilka wartości ma sens a reszta już niekoniecznie.

class Month(private val n:Int) extends AnyVal{
  override def toString: String = s"Month($n)"
}

W teorii rozszerzenie AnyVal powinno zapobiec alokacji nowego obiektu na stercie czym zajmie się kompilator zastępując nasz domenowy Month zwykłym intem w trakcie kompilacji. Niestety ta reguła ma wiele wyjątków i czasem alokacja następuje wbrew naszym oczekiwaniom. Zejdziemy teraz dużo niżej by lepiej zrozumieć kiedy coś takiego następuje..

Poligon

Naszym Poligonem laboratoryjnym będzie domenowy Kalendarz, który operuje na domenowym Miesiącu (wszystko takie domenowe)

class Calendar{
  def somethingWithDate(m:Month) = {
    println(m)
  }
}

I po dekompilacji.

Compiled from "Poligon.scala"
public class jug.workshops.reactive.akka.typed.Calendar {
  public void somethingWithDate(int);
  public jug.workshops.reactive.akka.typed.Calendar();
}

Na pozór kod po dekompilacji wygląda ok, zamiast Month mamy Int i wydawać by się mogło, że wszystko idzie zgodnie z planem ale czeka nas niemiła niespodzianka...

Trochę mechaniki

Gdy zanurkujemy do asemblera to zobaczymy :

  Code:
       0: getstatic     #17                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
       3: new           #19                 // class jug/workshops/reactive/akka/typed/Month
       6: dup
       7: iload_1
       8: invokespecial #22                 // Method jug/workshops/reactive/akka/typed/Month."<init>":(I)V
      11: invokevirtual #26                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
      14: return

Gdzie w linijce 3 pojawia się new czyli tworzenie nowego obiektu. Jest to zapewne Kochani moi polimorfizm w akcji gdzie println gdzie pod spodem musi być wywołane toString ale ze względu na dziedziczenie nie jest jasne z której klasy i tam cała ta maszyneria idzie w ruch. Bardzo łatwo pozbyć się alokacji usuwając polimorfizm ze sceny.

class Month(private val n:Int) extends AnyVal {
  def display: String = s"Month : $n"
}

class Calendar{
  def somethingWithDate(m:Month) = {
    println(m.display)
  }
}


 public void somethingWithDate(int);
    Code:
       0: getstatic     #17                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
       3: getstatic     #22                 
// Field jug/workshops/reactive/akka/typed/Month$.MODULE$:Ljug/workshops/reactive/akka/typed/Month$;
       6: iload_1
       7: invokevirtual #26                 
// Method jug/workshops/reactive/akka/typed/Month$.display$extension:(I)Ljava/lang/String;
      10: invokevirtual #30                 
// Method scala/Predef$.println:(Ljava/lang/Object;)V
      13: return

Po tej wycieczce po alokacjach wracamy do pattern matchingu.

Pattern Matching bez kosztu

Pattern matching bazuje na dekompozycji obiektów i metodzie unapply, która jest takim lustrzanym odbiciem kompozycji i metody apply. Kompilator tutaj dużo działa w tle bo gdy napiszemy poniższą linijkę.

case class Day(v:Int)

No i każdy początkujący adept scali pewnie wie, że będzie z automatu wygenerowany comanion object, ktory ma 40000 metod a wśród nich unapply do pattern matchingu. Ten unapply ma tę wadę, że tak trochę niepotrzebnie za każdym razem Optiona tworzy który zaraz idzie do kosza.

public final class jug.workshops.reactive.akka.typed.Day$ extends ... {
...
  public scala.Option<java.lang.Object> unapply(jug.workshops.reactive.akka.typed.Day);
...
  }

I tutaj pierwsza niespodzianka dla mnie bo okazuje się, że jednak ta metoda przy case class wcale nie jest używana!!!

Według :

However, when we get to pattern matching (§8.1), case classes have their own section on matching, §8.1.6, which specifies their behaviour in pattern matching based on the parameters to the constructor, without any reference to the already-generated unapply/unapplySeq:

Oraz:

I can tell you that even though the compiler generates an unapply method for case classes, when it pattern matches on a case class it does not use that unapply method

No i faktycznie żadnego unapply w asemblerze nie widać. Dopiero gdy zmienimy case class na zwykłą klasę to unapply pójdzie w ruch z alokacją Optiona.

class Day(val v:Int)

object Day{
  def unapply(arg: Day): Option[Int] = Some(arg.v)
}
I asembler :
63: invokevirtual #45                 
// Method jug/workshops/reactive/akka/typed/Day$.unapply:(Ljug/workshops/reactive/akka/typed/Day;)Lscala/Option;


 public scala.Option<java.lang.Object> unapply(jug.workshops.reactive.akka.typed.Day);
    Code:
       0: new           #17                 // class scala/Some
       3: dup
       4: aload_1
       5: invokevirtual #23                 
// Method jug/workshops/reactive/akka/typed/Day.v:()I
       8: invokestatic  #29                 
// Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
      11: invokespecial #32                 
// Method scala/Some."<init>":(Ljava/lang/Object;)V
      14: areturn

Ale po co tam option?

Odsuwając ideologie programowania w kont - z punktu widzenia pattern matchingu to option trochę tak z ch*ja jest. A to dlatego, że jeśli o sam Pattern Matching chodzi to my jako programiści tego Optiona nie tykamy, nie widzimy, nie wykorzystujemy - to je taka wewnętrzna rzecz - i w złym świecie programowania można to opykać bez alokacji na parze wartość/null - pewnie usuną z czatu monadowego ale garbage collector powinien chodzić trochę lepiej - co kto lubi i potrzebuje emocjonalnie w życiu.

Zastosowanie

Jak użycie tego wygląda w Akka (Acce) ? W ActorContextImpl (ku*wa lata uczenia młodzieży by nie dawała w nazwach Impl i ch** wszystko strzelił) znajdziemy

 private var messageAdapterRef: OptionVal[ActorRef[Any]] = OptionVal.None

I dalej mamy pattern matching

val ref = messageAdapterRef match {
case OptionVal.Some(ref) ⇒ ref.asInstanceOf[ActorRef[U]]
case OptionVal.None ⇒ ...
    }

Tak jak ja to osobiście rozumiem - działają w tym miejscu dwa extractory. Pierwszy bezpośrednio w OptionVal. Ale ale zaczynają dziać się rzeczy ciekawe bo żadnego unapply tam nie ma - znajdziemy dwie metody potrzebne do tego całego "name based extraction".

private[akka] final class OptionVal[+A >: Null](val x: A) extends AnyVal {
...

 def get: A =
    if (x == null) throw new NoSuchElementException("OptionVal.None.get")
    else x

  
  def isEmpty: Boolean =  x == null
...
}

Wracamy do naszego laboratorium by sprawdzić co takiego dzieje się pod spodem. Na początek przypomnijmy co mamy :

class Month(private val n:Int) extends AnyVal {
  def isEmpty:Boolean = n < 0 || n > 12
  def get:Int = n
  def display: String = s"Month : $n"
}

object Month{
  def unapply(v: Int) = new Month(v)
}

Co umożliwi nam poniższe zabawy.

    val Month(n) = 2
    println(n)


    1 match {
      case Month(n) => println(n)
      case _ => println("empty")
    }

    3 match {
      case Month(n) => println(n)
      case _ => println("empty")
    }

Fajnie jest ale teraz zmodyfikujmy nasze unapply by przyjmowało bezpośrednio obiekt domenowy, wtedy będzie bardziej biznesowo i domenowo.

object Month{
  def unapply(v: Month) : Month = v
}

Co już nam daje znak, że Option jako taki z równania został wyłączony. Ale czy na pewno? Jest tylko jeden sposób by sprawdzić.

- kod wysokopoziomowy

def somethingWithDate(m:Month) = m match {
    case Month(n) if n <=6 => println("first half")
    case Month(n)  => println("second half")
    case _ => println("error")
  }

kod (a właściwie kawałek) niskopoziomowy

  public void somethingWithDate(int);
    Code:
       0: iload_1
       1: istore_3
       2: getstatic     #17                 
// Field jug/workshops/reactive/akka/typed/Month$.MODULE$:Ljug/workshops/reactive/akka/typed/Month$;
       5: iload_3
       6: invokevirtual #21                 
// Method jug/workshops/reactive/akka/typed/Month$.unapply:(I)I
       9: istore        4
      11: getstatic     #17                 
// Field jug/workshops/reactive/akka/typed/Month$.MODULE$:Ljug/workshops/reactive/akka/typed/Month$;
      14: iload         4
      16: invokevirtual #25                 
// Method jug/workshops/reactive/akka/typed/Month$.isEmpty$extension:(I)Z
      19: ifne          57
      22: getstatic     #17                 
// Field jug/workshops/reactive/akka/typed/Month$.MODULE$:Ljug/workshops/reactive/akka/typed/Month$;
      25: iload         4
      27: invokevirtual #28                 
// Method jug/workshops/reactive/akka/typed/Month$.get$extension:(I)I
      30: istore        5
      32: iload         5
      34: bipush        6
      36: if_icmpgt     54
      39: getstatic     #33                 
// Field scala/Predef$.MODULE$:Lscala/Predef$;
      42: ldc           #35                 // String first half

I jak to przestudiujecie to nie powinniście tam nigdzie znaleźć żadnego new a jedynie wywołania statyczne.

I wydaje mi się, że to co jest w Acce idzie jeszcze dalej. Przypomnijmy kod :

val ref = messageAdapterRef match {
      case OptionVal.Some(ref) ⇒ ref.asInstanceOf[ActorRef[U]]
      case OptionVal.None

I teraz jeśli chodzi o None to jest to zwykły obiekt z tym isEmpty oraz get.

val None = new OptionVal[Null](null)

I tutaj weź głęboki wdech i przypomnij sobie nasz poprzedni przykład z miesiącem. Dla Pattern Matchingu istotne jest co będzie zwrócone z unapply - żeby to miało get i isEmpty - i tutaj wymijamy unapply i od razu podrzucamy taką instancję singletona.

A co z tym drugim Kejsem? Tutaj mamy ładne customowe unapply.

  object Some {
    def apply[A >: Null](x: A): OptionVal[A] = new OptionVal(x)
    def unapply[A >: Null](x: OptionVal[A]): OptionVal[A] = x
  }

Po co to?

W moim odczuciu AnyVal to w pewnym stopniu udana próba implementacji mechanizmu przeniesienia typów prymitywnych do warstwy domenowej bez zbytniego cierpienia w runtime. Ponieważ koniec końców tam na dole zawsze będzie JVM to często czar pryska i trzeba instancję stworzyć, dlatego też zazwyczaj używałem AnyVal jako zabezpieczenia w sygnaturze. Tutaj pojawia się nowa ciekawa możliwość próby zaimplementowania takiego prostego i opartego na prymitywach ADT co widzieliśmy na przykładzie Akki gdzie działa taki Some/None jako nakładka na ActorRef i null.

Pojawiają się takie brzydkie słowa jak var czy null ale może to nie czas by się spuszczać nad programowaniem ideologicznym i pomyśleć, że ci ludzie oszczędzili tak ileś tam pamięci, którą musieli oszczędzić i poszli na piwo.

Podsumowanie

Także drodzy przyjaciele zachęcam do eksperymentów bo jak wspominałem na samym początku i dla mnie to była edukacja. Wrzućcie czasem kawałek kodu, odpalcie "javap -c" i zobaczcie co się tak naprawdę dzieje pod spodem. No a jak potrzebujecie inspiracji - to zerknijcie w jakiś kod pisany przez mądrych ludzi - najlepiej kod który zarabia jakieś pieniądze.

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.