środa, 11 maja 2016

Typy Danych ale Algebraiczne

Często nie ma co zbyt pochopnie wydawać oceny gdyż wiele zależy od sytuacji i kontekstu :

  • Widzisz kolesia z tasakiem we krwi - krzyczysz "Policja!" - a to tylko rzeźnik wraca z fajki.
  • Widzisz jak koleś wpada do banku z kałachem - krzyczysz "Policja!" - a to tylko kręcą "Nowe przygody Porucznika Borewicza"
  • Widzisz kolesia, który w klasie abstrakcyjnej jawnie odwołuje się do typów, które ją rozszerzają - krzyczysz "Policja Obiektowa !" - a to tylko ktoś sobie implementuje Algebraiczne Typy Danych

"to zależy"

Programiści to ludzie a ludzie lubią jak są jasne zasady, posortowane ,dobro-zło, po przydzielane do danej kategorii i zaszufladkowane. "Czy to jest dobre rozwiązanie? To zależy..." - to niektórych bardzo denerwuje i frustruje bo psuje ścisłe prawa rządzące ich światem.

Ta książka :
Strona 117 (edycja polska, wydawnictwo Helion, copyrajt 2010 itd) :

Jeśli dobrze kojarz Unkel Bob prowadzi firmę "ObjectMentor" (dla młodzieży - Unkel Bob - Robert C.Martin - autor Clean Code) no i jak on sam twierdzi, że nie wszystko jest obiektem i czasem jak coś nie jest obiektem to dobrze, że nie jest obiektem - to może również na pytanie "czy dobre zasady programowania obiektowego są zawsze dobre " - może też zależy? (jak np. "czy bieganie jest zdrowe? No jak stoisz na polu minowym to nie jest- także zależy.")

"Otwarty na rozbudowę / zamknięty na polemikę"

Jest zasada Otwarty na rozbudowę / zamknięty na modyfikację i ogólnie jest to dosyć dobra zasada ale problem polega na tym, że można ją interpretować. Dokładnie tak - programiści ją interpretują i stosują swoją interpretację. Moją interpretacją przez kilka ładnych lat było to, że "trzeba tak napisać klasy i interfejsy, żeby łatwo nowe klasy implementowały te interfejsy a stare klasy były nie ruszane".

Jednak można znaleźć przykłady gdzie zabronienie powyższego procederu tylko wyszło na dobre! Np w uproszczeniu Option w Scali wygląda tak :

sealed trait Option[+A]
final case class Some[+A](get : A) extends Option[A]
case object None extends Option[Nothing]

Sealed to bardzo interesujący mechanizm, którego chyba nie ma w Javie. To takie final ale tylko dla kodu, który nie znajduje się w tym samym pliku - sprawia to, że możemy stworzyć lokalną rodzinę typów zamknięta na dalsza rozbudowę (rozbudowę w sensie dokładania nowych typów bo można o nowe operacje). Nie jestem historykiem-archeologiem ale wydaje mi się, że ten Option jest w Scali od dosyć dawna. W Javie jest Optional, które jest "public final class" i tez ogólnie na rozszerzanie jest zamknięte (i może własnie brak mechanizmu sealed w Javie spowodował, że tam Optional jest zaimplementowany jako taka wolno stojąca pojedyncza klasa ? Nie wiem, być może - to taki strzał z dupy)

Jest jeszcze inny ciekawy przykład...

Jaka cena?

To jest przykład , który z jednej strony ma szanse pokazać, że i czasem przez ponad 150 lat nie ma potrzeby na "rozbudowę typu". I pomimo, że typ nie był rozbudowywany to jest niezwykle użyteczny i praktyczny. Do tego cały czas się ciągnie to słowo Algebraiczne, które brzmi bardzo abstrakcyjnie i matematycznie.

Ów przykład to Algebra Boole'a - i pojawia się tam to słowo Algebra a generalnie może się pojawić bo typy i operacje są jasno określone bo ograniczone . Co by się stało gdyby typ Boolean był otwarty na rozbudowę w rozumieniu "dodawania nowych typów rozszerzających Boolean?"

Jak mamy :

if(costam) {
wariant1
}else{
wariant2
}

To wiadomo, że albo "costam" to albo false albo true (poza językami, gdzie pusta tablica to też false ale to inna historia). I przez to, że jest tak ubogo to jest prosto, a jak jest prosto - to jest bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo dobrze.

Jak jest prosto to jest bardzo dobrze i tworzenie abstrakcji gotowej na rozbudowę może mieć swoje uzasadnienie ale ma tez swoją cenę!. I jak np. napiszemy kod bardzo abstrakcyjny to nie jest tak, że "dobre bo solid kurde solid" tylko trzeba zadać tez sobie pytanie czy jesteśmy świadomi ceny i czy potrzeba tę cenę uzasadnia.

Czyli to że Boolean to tylko True i False to dobrze - uwaga! "To zależy" . Tak na codzień to chyba dobrze ale już w poniższych linkach wygląda tak trochę jakby nie wystarczyło

Koniec filozofowania

Dobra pisanie tekstu zamiast kodu ma tę wadę, ze muszę potem sprawdzić czy nie ma w nim błędów bo spellcheckerowi daleko do kompilatora. Także tego...

W Scali jakbyśmy chcieli takiego Booleana rezem z operacjami zaimplementować to tak :

sealed trait Bul{
  def and(other:Bul)= this match {
    case Fols => Fols
    case Tru  => other
  }
}
case object Tru extends Bul
case object Fols extends Bul

I teraz mamy rzecz niesłychaną!!! Oto w klasie (a właściwie traicie) abstrakcyjnej mamy Pattern Matching z konkretnymi typami. Ale słuchaj, słuchaj, wdech-wydech, posłuchaj. Wyłącz na chwile reakcję automatyczną ciała migdałowatego i pomyśl - i tak nie dorzucimy żadnego nowego typu, wszystko jest jasno określone tu i teraz więc czemu by tej wiedzy nie wykorzystać? Ano wykorzystujemy.

I w scali fajne jest to, co jeśli źle użyte będzie przeklęte - można czasem olać kropki i nawiasy i wtedy mamy :

println(Tru and Tru) //Tru
println(Tru and Fols) //Fols
No dla mnie wygląda to fajnie. Ale można jeszcze inaczej :
object BulAlgebra{
  def and(left:Bul,right:Bul):Bul=(left,right) match{
    case (Fols,Tru) => Fols
    case (Tru,Tru) => Tru
  }
}
I jest tutaj specjalnie zrobiony błąd bo nie wszystkie opcje są rozpatrzone :
Warning:(29, 35) match may not be exhaustive.
It would fail on the following inputs: (Fols, Fols), (Tru, Fols)
  def and(left:Bul,right:Bul):Bul=(left,right) match{
                                  ^

I zobaczcie co zyskaliśmy przez to, że nie tolerujemy open/close principle po obiektowemu - wszystkie typy są w jednym miejscu i kompilator może mi zwrócić pożyteczną uwagę "o czymś żeś zapomniał i patrz wyjebie się - nie ma za co"

To była Scala a teraz coś z zupełnie innej beczki

Idris

Idris to język - który wygląda jak Haskell plus ma bonusy - taki Super Sajan Haskellz rodziny "bardzo dobrze zaprojektowane ale mało kto umie używać" (tego typu języki napotykają dziwną barierę w organizacjach gdzie "wzrost" jest mierzony "hed kantem"). I generalnie tutaj to normalnie jest Baaaaaaaaaaajka (nie mylić z tym klubem w Mielnie).

data Bul = Tru | Fols

and : (Bul,Bul) -> Bul
and (Tru, Fols) = Fols
and (Tru,Tru) = Tru

I znowu jest specjalny błąd ale tutaj do problemu podejdziemy trochę od innej strony. W Idris możemy sprawdzić czy funkcja jest całkowita czy częściowa gdyż kompilator ma wszystkie informacje o typie - fachowo to się nazywa totality checker.

I po uzupełnieniu :

and : (Bul,Bul) -> Bul
and (_, Fols) = Fols
and (Fols, _) = Fols
and (Tru,Tru) = Tru

I bardzo dobrze. I naprawdę podoba mi się ta nazwa "Totality Checker" - jak taki cyfrowy superbohater.

Kotlin

Kotlin jest o tyle ciekawym językiem, że został stworzony przez tę samą firmę, która stworzyła jedno z lepszych (a być może najlepsze) IDE dla programistów Javy - JetBrains. I to co jest niezwykle ciekawe,to że firma z kilkunastoletnim doświadczeniem z parsowania na lewo i prawo składni Javy stworzyła dla swojego języka składnię podobną bardziej...do Scali. Dokładnie tak - Kotlin jest podobny bardzo do Scali niż Javy - a do tego JetBrains reklamuje go jako język bardzo praktyczny i ma wszelkie "kwalifikacje" aby twierdzić, że wie jak powinien wyglądać język praktyczny. Poniżej przykład ADT w Kotlinie - co moim zdaniem jest bardzo dobre to fakt, że jeśli ten ichniejszy pattern matching z "when" ma brakujące przypadki to kod zwyczajnie się nie kompiluje.

sealed class Bul{
    object Tru : Bul()
    object Fols : Bul()

    fun and(other:Bul):Bul=when(this){
        is Fols -> Fols
        is Tru -> other
    }
}

Także może należy przenieść dyskusje "składnia Javy i składnia Scali" na nowy poziom - "Składnia języków z lat 90tych i składnia nowych języków?". Generalnie kto zna Scalę powinien szybko załapać Kotlina a kto zna Javę -cóż... będzie miał okazję poznać wiele mechanizmów, o których istnieniu nie miał pojęcia.

wtorek, 3 maja 2016

For comprehension ale że w Javie

Pytanie czy w sytuacji gdy chcę napisać po prostu kilka kawałków kodu potrzebny jest jakiś rozbudowany wstęp? Sam sobie odpowiem, że nie i dlatego tylko wspomnę, że będzie tutaj o konstrukcji, która może znacznie poprawić czytelność kodu gdy w naszym kodzie Javowym (tak Javowym) pojawi się koło siebie kilka Optionów czy Futureów.

W Javie8 i są już i lamby i są już reprezentacje "Efektów". I chyba każdy język, który ma lambdy i efekty ma coś co nazywa się "[coś-tam] comprehension" - no tylko Java nie ma... ale w sumie to trochę ma... ale nie do końca. Zresztą zaraz zobaczymy.

Po co w ogóle to comprehension

Nie będziemy się bawić w tworzenie jakiejś sztucznej domeny tylko po prostu jedziemy z trzema optionami.

val option1=Some(1)
val option2=Some(2)
val option3=Some(3)

Jak mamy taki option to możemy kombinatorem/metodą map przetwarzać potencjalną wartość, która weń siedzi przy pomocy takich "czystych" normalnych funkcji.

option1.map(i=>s"result is $i").getOrElse("there is no result")
//res1: String = result is 1

To było proste ale bardziej skomplikowane konstrukcje pojawią się gdy więcej Option-ów musi ze sobą współpracować.I jeśli chce dodać dwa inty z option1 i option2 to będzie to wyglądać tak :

option1.flatMap{i1 =>
        option2.map(i2=>i1+i2)
}

I analogicznie dla trzech :
val result: Option[String] = 
 option1.flatMap { i1 =>
        option2.flatMap { i2 =>
                option3.map(i3 => s"result is  ${i1 + i2 + i3}")
        }
 }
I tak dalej...

Ponieważ im więcej tego będzie tym większy arrow code się wygeneruje dlatego też w Scali mamy do dyspozycji taką oto konstrukcję :

val forResult: Option[String] =for{
        i1 <- option1
        i2 <- option2
        i3 <- option3
} yield s"result is ${i1+i2+i3}"

result.getOrElse("there is no result")
Wynik ten sam a jak się zna ten piękny pokazywany tutaj język to czyta się o niebo łatwiej.

A co jest w Javie? A w Javie...

Mroczne widmo

Przy pomocy standardowych bibliotek w Javie to "se" można takie coś najwyżej zrobić :

Optional<Integer> optional1 = Optional.of(1);
Optional<Integer> optional2 = Optional.of(2);
Optional<Integer> optional3 = Optional.of(3);


Optional<String> result =
        optional1.flatMap(i1 ->
            optional2.flatMap(i2 ->
                optional3.map(i3 -> "result is : " + (i1 + i2 + i3))
            )
        );

System.out.println(result.orElse("there is no result"));

// result is : 6

Ale nie jest jeszcze tak źle bo ten... epizod sżósty... i tak dalej

Nowa nadzieja

Jacyś bardzo dobrzy ludzie - niech im szczęście w życiu sprzyja - stworzyli bibliotekę JAVASLANG , która po pierwsze primo daje nam typ Option, który nie jest tka upośledzony jak Optional z Javy8 (z 5 razy więcej przydatnych nań jest metod). No i bardzo wygodnie się pomiędzy tymi dwoma mapuje.

Option<Integer> dobryOption = Option.ofOptional(Optional.of(1));
Optional<Integer> dziwnyOption = dobryOption.toJavaOptional();

Ale najważniejsze jest to teraz, patrz na to , ej no skup się teraz, przypomnij sobie te flaMapy z poprzedniego punktu i patrz na to, patrz teraz na to :

 API.For(Option.of(1),Option.of(2),Option.of(3))
                .yield((i1,i2,i3)->i1+i2+i3)
                .map(sum -> "result is : "+sum)
                .forEach(System.out::println);
//result is : 6

To jest chyba nówka sztuka : http://static.javadoc.io/io.javaslang/javaslang/2.0.2/javaslang/API.html

Labola.. Rabor... Rabol ... Labolatorium

Scala - wszystkie Optiony Some

for {
      i1 <- Option(1)
      i2 <- Option(2)
      i3 <- Option(3)
    } yield i1+i2+i3

//Some(6)

Javaslang - wszystkie Optiony Some

API.For(Option.of(1), Option.of(2), Option.of(3))
                .yield((i1, i2, i3) -> i1 + i2 + i3).headOption()

//Some(6)

Scala - jeden Option None

val result: Option[Int] =for {
      i1 <- Option(1)
      i2 <- Option.empty[Int]
      i3 <- Option(3)
    } yield i1+i2+i3

//None

JavaSlang - jeden Option None

Option<Integer> result = API.For(Option.of(1), Option.<Integer>none(), Option.of(3))
                .yield((i1, i2, i3) -> i1 + i2 + i3).headOption();

//None

Scala - Interakcja pomiędzy Optionami

 val result: Option[Int] =for {
      i1 <- Option(1)
      i2 <- Option(2 + i1)
      i3 <- Option(3 + i2)
    } yield i1+i2+i3

//Some(10)

Javaslang - Interakcja pomiędzy Optionami

Tutaj trzeba zwrócić uwagę, że składnia jest ciutkę inna ale z kompilatorem Javy chyba nic lepszego się nie zdziała :(

Option<Integer> result = 
        For(Option.of(1), i1 ->
                For(Option.of(2 + i1), i2 ->
                        For(Option.of(3 + i2)).yield(i3 -> i1 + i2 + i3)
                )
        ).headOption();

//Some(10)

No to już tak średnio wygląda ale to nie największy problem...

"Ej a ten headOption?" - czyli nie ma róży bez kolców a ślimak pokaż rogi gdzie kucharek sześć

Trochę tam oszukiwaliśmy w tym Javaslang bo zawsze po wszystkim wołaliśmy "headOption". A stosowaliśmy to gdyż oryginalny typ to :

 Iterator<Integer>
Czyli dostajemy osobny typ zdefiniowany przez Javaslang
package javaslang.collection;

public interface Iterator<T> extends java.util.Iterator<T>, Traversable<T> {...}

Zaś sama deklaracja For wygląda tak :
 public static <T1, T2> For2<T1, T2> For(Iterable<T1> ts1, Iterable<T2> ts2)
Dlatego możemy zapomnieć o użyciu tutaj Optional czy CompletableFuture z Java8 (ale an szczęście są konwersje do javaslang i jest jakiś Future w Javaslang ale nie stosowałem)

Jakie to ma konsekwencje? Np w Scali gdy zrobię obliczenia na typie , który niesie ze sobą więcej informacji niż jest/nie ma to raz, że nie trace informacji o typie a dwa to, że nawet w przypadku failu wiem co się stało.

val result: Try[Int] = for {
      i1 <- Try(1)
      i2 <- Try(1 / 0)
      i3 <- Try(3)
    } yield i1 + i2 + i3

//Failure(java.lang.ArithmeticException: / by zero)
A Javie niestety w kontekście obydwu wymienionych punktów możemy rozłożyć ręce...
Iterator<Integer> result = For(
                Try.of(() -> 1),
                Try.of(() -> 1 / 0), 
                Try.of(() -> 3))
                .yield((i1, i2, i3) -> i1 + i2 + i3);

//Iterator()
Ale i tak to co możemy zrobić z biblioteką Javaslang jest o niebo lepsze niż z samą biblioteką standardową. A pod tym linkiem jest więcej ciekawych inicjatyw : 5 Essentials for a Java 8 Tech Stack

11 Maja będzie warsztat o efektach ubocznych w Javie to będzie można sobie poćwiczyć : Warsztaty Java8 - Funkcje i Efekty Uboczne