poniedziałek, 17 kwietnia 2017

Nauka Modelowania Domeny z Programowaniem Funkcyjnym

Co będzie

  • Będzie warsztat FP + modelowanie domeny-> Modelowanie Domeny z FP - część 1 - Oddzielenie biznesu od efektów ubocznych. Najprawdopodobniej 10 maja ale wiele rzeczy się dzieje to i termin może się przesunąć.
  • na podstawie książki
  • w Scali
  • przykłady praktyczne z wykorzystaniem czasem zaawansowanego FP
  • nie będzie hajbernejta ani żadnych frameworków (nazywanych przez ludzi pozbawionych praktyki programistycznej "zrębami"), które gloryfikują kod gdzie jest więcej annotacji niż samego kodu -> czyli czegoś w stylu :
    @ManyToMany
    @JoinTable(
          name="EMP_PROJ",
          joinColumns=@JoinColumn(name="EMP_ID", referencedColumnName="ID"),
          inverseJoinColumns=@JoinColumn(name="PROJ_ID", referencedColumnName="ID"))
    @WhatDoYouMeanByLearnSQL?
    @FuckThoseRelationsOOPUberAlles
    @IJustWantToHaveAListAndCallGetterOnIt
    @IDontCare
    @PleaseDontForceMeToThink
    @MagicalAnnotationToMakeAllProblemsDissapear
    @AndAlsoPleaseHandleTransactionsForMe
    private List projects;
    
    (co w moich oczach przypomina bardziej średniowieczną alchemię gdzie dąży się do takiego dobrania składników by zamienić metal w złoto - w tym przypadku zamienić zwykłą deklarację listy w jakiś twór, który spełni niebanalne wymagania domenowe)

A dokładniej

Traktujac ksiązkę jako kręgosłup nauki pojawia się następujący plan:

  1. Tworzenie danych/aggregatów i oddzielenie operacji domenowych od operacji na efektach infrastruktory - Option,Try,Future,Validation -> i jak map i flatMap pomagają utworzyć przepływ danych
  2. Lenses i kompozycja przekształceń na agregacie danych
  3. Kompozycja operacji na repozytorium przy pomocy Monad Readera
  4. Type classes i monoid - to da wiedzę jak uzyskąć znacznie lepszą kompozycję poprzez definicję symbolicznych operacji dla danego typu
  5. Funktor,Applicative i Monady - jeszcze wiecej wiedzy jak uzyskać lepszą kompozycje funkcji biznesowych poprzez separację efektów ubocznych systemu. Tutja moga sie pojawiać zastosowania dla bardziej specjalistycznych typów jak StateMonad
  6. No i jedziemy dalej z coraz silniejsza kompozycją - tutja będzie tzw. “Kleisli arrow” , która to konstrukcja pozwala komponować typy jak M[A] => M[B] prz pomocy funkcji z efektami A=>M[B]
  7. Wykrywanie naruszeń zasad biznesowych w trakcie kompilacji - typy Fantomowe
  8. Modularyzacja/Bounded Context przy pomocy FreeMonad

Plan można zmieniać wedle upodobań. Formuła to zapewne dwie części z ćwiczeniami. Część pierwsze zajmie się bardziej mechanika używanych konstrukcji FP a druga bardziej zastosowaniem domenowym. No i formułę tez oczywiście można zmienić wedle upodobań. Także (najprawdopodobniej) 10 maja zapraszam kto chętny : Modelowanie Domeny z FP - część 1 - Oddzielenie biznesu od efektów ubocznych

niedziela, 26 lutego 2017

Closed/Closed Principle

(...)The short way of saying this is, OpenClosedPrinciple cannot work well without a good crystal ball. It is sort of like saying, "Stocks are easy, all you have to do is pick the right stock and you can be rich." It's a "tease" principle without teeth(...)

Wstęp

Jest kilka zasad w programowaniu, których się po prostu "nie rusza". Nie podważa, nie analizuje, nie szuka alternatyw.A jeśli tylko spróbujesz to zostaniesz wykluczony z kręgu towarzyskiego "programistów, którzy rozumieją jak się robi dobre programy". I teraz spróbuj nie dostać kamieniem gdy wejdziesz z opinią, że open/close principle (po naszemu "ino otworte na rozszerzonie ać z domykiem na modyfikacjom!") może w niektórych miejscach przynieść więcej szkody niż pożytku.

Co jakiś czas można się nadziać na artykuł o zasadzie otwarte/zamknięte gdzie logika argumentacji trochę przebiega jak w przykładzie "a teraz pokaże wam, że opony zimowe są lepsze od letnich. Na potrzeby artykułu załóżmy, że wszędzie leży śnieg". Czyli "o tutaj mam taki problem, który dobrze rozwiązuje dziedziczenie w javie. Dla potrzeb dyskusji przyjmijmy, że każdy problem tak wygląda". Mam nadzieję, że w tym artykule uda nam się zobaczyć, że rzeczywistość jest bardziej "różnorodna".

Ale zaczniemy od tego, że zazwyczaj przy okazji takich "praw" programowania gdzieś tam w tle pojawia się temat zmiany logiki/wymagań/funkcjonalności. I, że na zmianę trzeba się przygotować. Przygotowanie się na zmianę w miejscu gdzie ona nigdy nie nadejdzie może nas tak naprawdę sporo kosztować i wtedy zastosowanie "o/c" staje się zwykłym antywzorcem - a powiązany kawałek kodu zwykłą fuszerką. No ale jak to zmiana nie nadejdzie? Przecież w chaotycznie prowadzonym projekcie IT zmienia się wszystko! Nie. Nawet w tysiącach maksymalnie totalnie chujowo prowadzonych projektach jest kawałek logiki, który nigdy się nie zmienił i pewnie nie zmieni nigdy - i to od niego zaczniemy dalsze rozważania.

170 lat bez zmiany

(Albo 680 łokresów kwartalnych bez Rekłestu dla czendża w tejże logice)

Jeśli wierzyć internetowi (a dlaczego by nie wierzyć) Algebra Boola was introduced by George Boole in his first book The Mathematical Analysis of Logic (1847). Czyli dawno. Bardzo. I nic się nie zmieniło.

Wyobraźmy sobie teraz na chwilę, że nie ma algebry Boolea (bóla). Ni ma. Ni ma true. Ni ma false. I w tym świecie ktoś rzuca speckę by taki typ zrobić. Potencjalnie można stworzyć popularną javową konstrukcję z "interfejsem, implementacjami , polimorfizm i w ogóle"

interface Bool{
    Bool or(Bool o);
    Bool and(Bool o);
}

class True implements Bool{

    @Override
    public Bool or(Bool o) {
        return new True();
    }

    @Override
    public Bool and(Bool o) {
        return o;
    }
}

class False implements Bool{

    @Override
    public Bool or(Bool o) {
        return o;
    }

    @Override
    public Bool and(Bool o) {
        return new False();
    }
}

I chociaż technicznie taka implementacja powinna działać to jest ona bez sensu. Z bardzo prostego powodu. Chociaż dodanie nowego typu jest możliwe od strony mechaniki języka to od strony funkcjonalności to nie ma szans zadziałać. No bo jak byśmy dodali typ AlmostFalse implements Bool to teraz jak ma zmienić się zachowanie metod and i or?

Ponieważ typ jako taki
jest częścią API
to dając nowy
czeka nas zawrót głowy

W skrócie nie będzie działać. Dlatego tez if w javie od 20 wygląda tak samo. Jest albo true albo false. Nie ma nic innego i nie ma w planach niczego innego. If - i wszystko co bazuje na boolean jako boolean - jest zamknięte na rozbudowę i zamknięte na rozszerzanie. I całe szczęście. Inaczej mało co by działało i reaktory by wybuchały.

W jednym z popularniejszych przykładów Open/Closed jest chyba ten gdzie dodaje się nowe figury geometryczne i tak mając interfejs "Figura" (Katarzyna) z metodą pole , mamy trójkąt i możemy sobie dodać kółko. Przykład jest wygodny i ma tę właściwość, że typy nie oddziałują same ze sobą czyli nie ma na tej płaszczyźnie pomiędzy nimi relacji. Relacji w rozumieniu abstrakcyjnym. I właśnie wokół tego słowa i ogólnie pojętej abstrakcji na chwile przejdziemy do mniej praktycznych rozważań.

Nazwy

Jak tam wyżej wspominaliśmy Boole'a" to tam był jeszcze chyba mianownik "kto?co? -> Algebra" . Algebra jest słowem niezwykle abstrakcyjnym i chyba nie atrakcyjnym dla szerokiej masy ludzi, którzy mówiąc "w życiu trzeba spróbować wszystkiego" mają zazwyczaj na myśli narkotyki i skoki z samolotu a nie zrozumienie Analizy Matematycznej (czy jakoś tak szedł ten dowcip).

Dlatego zamiast o abstrakcyjnych pojęciach matematycznych ruszymy łatwiejszą drogą, wkleję zdjęcie czaszki i porozmawiamy o grze.

Gra ciekawa, wydana ze 20 lat temu o tytule "Planescape torment" - główny bohater nie dość, że nie żyje to nie ma imienia i to właśnie temat "znaczenia imienia/nazwy" jest tam jednym z głównych wątków. Z tego co pamiętam w pewnym momencie spotykało się typka co to przeklinał dzień kiedy odzyskał imię bo od tego dnia właśnie jego wrogowie mogli rzucać w niego klątwami czy coś w tym stylu.

Podobnie możemy podejść do kwestii nazewnictwa przy rozważaniu praw programowania. Być może lepiej zahaczać o pewne pojęcia bez definiowania konkretnej nazwy. Ominiemy w ten sposób pole minowe nafaszerowane subtelnymi niuansami znaczeniowymi i unikniemy jałowych dyskusji w stylu "czy Try to Monada" i takie tam.

ADT po raz pierwszy

Jak już pojawia się słowo Algebra niedaleko padają od jabłoni Algebraiczne Typy Danych i - czego się spodziewaliśmy - tuzin różnych definicji. Można sobie o tym poczytać na wikipedi https://en.wikipedia.org/wiki/Algebraic_data_type . Sam również kiedyś próbowałem to opisać w sposób prawidłowy i zabawny tutaj -> http://pawelwlodarski.blogspot.com/2016/05/typy-danych-ale-algebraiczne.html. Nie wiem czy udało mi się jedno albo drugie.

Skacząc dalej po linkach ciekawe zdanie jest umieszczone na haskellowej wiki : https://wiki.haskell.org/Algebraic_data_type

This is a type where we specify the shape of each of the elements(...)

Czyli rozumiejąc tak jak jest mi na tę chwilę wygodnie będzie to typ, gdzie wszystkie elementy są jasno określone. Czyli dla typu Boolean jasno określone są True i False. Tylko, że znowu może to znaczy to a może coś innego. Jaki jest rozpierdziel z definicjami doczytamy w dalszych akapitach zacytowanej strony.

Algebraic Data Type is not to be confused with *Abstract* Data Type, which (ironically) is its opposite, in some sense. The initialism "ADT" usually means *Abstract* Data Type, but GADT usually means Generalized *Algebraic* Data Type.
Także tego...

ADT po raz drugi (i PDT)

Link do opracowania : https://www.cs.utexas.edu/users/wcook/papers/OOPvsADT/CookOOPvsADT90.pdf

Wspomniana tutaj praca zestawia ze sobą reprezentacje typów programowania obiektowego zwaną dalej PDT - Procedural Data Types z innym ADT - Abstract Data Types. Przyswajania informacji wcale nie ułatwia fakt, że cytowana w poprzednim punkcie wiki Haskella twierdzi, że Abstract Data Types to coś przeciwnego do Algebraic Data Types a autorzy tego opracowania twierdzą, że jest "complementary" do programowania obiektowego.

Praca ma stron 20 dlatego wybierzemy sobie z niej jeden interesujący aspekt odnośnie dwóch różnych podejść do modelowania danych.

ADTs are organized around the observations. Each observation is implemented as an operation upon a concrete representation derived from the constructors. The constructors are also implemented as operations that create values in the representation type. The representation is shared among the operations, but hidden from clients of the ADT.

PDA is organized around the constructors of the data abstraction. The observations become the attributes, or methods, of the procedural data values. Thus a procedural data value is simply defined by the combination of all possible observations upon it.

Definicje znowu sa bardzo abstrakcyjne ale na szczęście w artykule mamy przykłady konkretnych deklaracji listy zarówno w zgodzie z PDT jak i ADT. Lista koncepcyjnie jako lista będzie też czymś co może zaskoczyć programistów "tylko javy" bo będzie ona reprezentowana jako dwa pod typy "ELEMENT_LISTY" oraz "KONIEC_LISTY"

PDT

Nil = recursive self = record
  null? = true
  head = error;
  tail = error;
  cons = fun(y) Cell(y, self);
  equal = fun(m) m.null?
end
Cell(x, l) = recursive self = record
  null? = false
  head = x;
  tail = l;
  cons = fun(y) Cell(y, self);
  equal = fun(m) (not m.null?)
    and (x = m.head)
    and l.equal(m.tail) 
end

Nie mogłem namierzyć info czy to jakiś konkret język czy taki edukacyjny pseudokod ale nie ma to znaczenia bo PDF i tak go nie skompiluje. Ten kawałek tutaj przypomina podejście "polimorfizm ala Java" ino trzeba tam jakiś interface List dodać. Mamy pod-typy i każdy po swojemu przeciąża metodę. Czyli tak jak napisaliśmy wcześniej : "Thus a procedural data value is simply defined by the combination of all possible observations upon it." A teraz zobaczmy to samo inaczej

ADT

adt IntList
representation
  list = NIL | CELL of integer * list
operations
  nil = NIL
  adjoin(x : integer, l : list) =
    CELL(x, l)
  null?(l : list) = case l of
    NIL ⇒ true
    CELL(x, l) ⇒ false
  head(l : list) = case l of
    NILerror
    CELL(x, l ) ⇒ x
  tail(l : list) = case l of
    NILerror
    CELL(x, l ) ⇒ l
  equal(l : list, m : list) = case l of
    NILnull?(m)
    CELL(x, l ) ⇒ not null?(m)
      and x = head(m)
      and equal(l , tail(m))

Tym razem bardziej to przypomina prosty zestaw typów z jasno zdefiniowanym zbiorem operacji, który można nań wykonać.Zwróć uwagę, że każda z operacji jest w pełni świadoma jakiego typu dane może dostać i stosuje nań standardowy pattern matching. Czyli tak jak napisalismy wcześniej : "Each observation is implemented as an operation upon a concrete representation derived from the constructors(...)The representation is shared among the operations, but hidden from clients of the ADT."

Praktycznie

Po tej teoretycznej wycieczce po abstrakcjach i ciekawych artykułach skupimy się na słowie, które już wcześniej padło odnośnie ADT i PDT a daje nadzieje na "win-win" czyli complementary - znaczy, że uzupełniający się. Będę starał się pokazać, że to nie jest żadne vs ale, że nasze standardowe podejście javowe może łatwo współpracować z czymś co łamie dobitnie zasadę open/closed.

Na początek zacznijmy ze standardową sytuacją, gdzie faktycznie chcemy dać sobie furtkę do rozszerzania danej logiki przez dodanie kolejnych implementacji "Biznesowej Walidacji". Będzie to mechanizm w zgodzie z O/C principle.

object BusinessLogic{
  trait BusinessData
  trait BusinessValidationPDT{
    def isValid(bd:BusinessData):Boolean
  }

  def validateProcess(data:BusinessData,checks:Iterable[BusinessValidationPDT]):Boolean =
    checks.forall(check => check.isValid(data))
}

Czyli tak jak z tymi figurami w oryginalnym przykładzie tak i tutaj mogę sobie dorzucić kolejną implementację. Mamy w tej sytuacji także druga rodzinę typów totalnie zamkniętych na wszelkie rozszerzania czyli już wcześniej wspomniany boolean. Teraz aby to rozróżnienie bardziej unaocznić zastąpmy boolean własnym typem, który po za tym, że coś się zepsuło niesie informacje co się zepsuło.

object CheckADT{
  sealed trait CheckResult
  final case object CheckOK extends CheckResult
  final case class CheckFailure(info:List[Throwable]) extends CheckResult

  def combine[A](checks:Iterable[CheckResult]):CheckResult = checks.foldLeft[CheckResult](CheckOK){
    case (CheckOK,CheckOK) => CheckOK
    case (CheckOK,f : CheckFailure) => f
    case (f : CheckFailure, CheckOK) => f
    case (CheckFailure(info1),CheckFailure(info2)) => CheckFailure(info1 ++ info2)
  }
}

Tym razem przy definicji traitu mamy slówko sealed czyli nie możemy dodawać nowych rozszerzeń poza plikiem gdzie ów trait jest zdefiniowany. Oznacza to w praktyce, że mamy dwa możliwe podtypy CheckResult i nic więcej. Teraz pytanie czy to ADT i jeśli ADT to Abstract czy Algebraic? Z punktu praktycznego nie ma to żadnego znaczenia i możemy go również nazwać "Andrzeja Dom Tonie" czy coś takiego. To co nas najbardziej interesuje to, że mamy jasno zdefiniowaną operacje na jasno zdefiniowanym zestawie typów : combine.

W zasadzie nic nie stoi na przeszkodzie by dodac kolejne operacje tak jak do boolean można dodac xora czy inne takie. Czyli cały czas pod pewnym aspektami jesteśmy przygotowani na rozbudowę ale nie kurwa tak, że fanatycznie wszystko polimorfizm i abstrakcje i polimorfizm i abstrakcje. Co więcej jest to ten moment kiedy można użyć Property Based Testing. Zazwyczaj przy okazji jakiejś prezentacji czy warsztatów ze ScalaCheck pada pytanie "no ale jak mam na przykład taką klasę User to jak do tego użyć PBT?". Źle zadane pytanie. Jak masz ADT i operacje na nich operujące wtedy możesz użyć PDT. (masa masa skrótów trzyliterowych, dobre na prezentacje marketingową, jeszcze tylko jakieś zdjęcia ludzi w garniakach ze stocka).

object prop extends Properties("CheckResult"){
  val gen:Gen[List[CheckResult]] = ???

    property("combine") =forAll(gen){checks=>
      val initialErrors=checks.collect{case  CheckFailure(infos) => infos.length}.reduce(_+_)
      val reducedErrors=CheckADT.combine(checks) match {
        case CheckOK => 0
        case CheckFailure(infos) =>  infos.length
      }
      initialErrors == reducedErrors
    }
  }

Czyli kolejna zaleta NIEstosowania o/c principle -> otwiera się przed nami potężny paradygmat testowania kodu jako zestawu twierdzeń.

No i na koniec jak to zastosować :

object BusinessLogic2{
  trait BusinessData
  trait BusinessValidationPDT{
    def isValid(bd:BusinessData):CheckResult
  }

  def validateProcess(data:BusinessData,checks:Iterable[BusinessValidationPDT]):CheckResult =
    CheckADT.combine(checks.map(_.isValid(data)))
}

Na razie chyba najlepsza publikacją omawiająca praktyczne podejście do tego co tutaj opisałem znalazłem w:

I jeszcze link do ciekawej prezentacji, żeby nie było że nikt inny nie jedzie po SOLID : why-every-element-of-solid-is-wrong

Podsumowanie

Wyobraź sobie, że to działa tak:

  1. Najpierw w ogóle nie wiesz, że jest open/close principle...
  2. Potem wiesz, że jest i tego powyżej uważasz za niekompetentnego programistę ale jeszcze nie wiesz, że jest kolejne stadium kiedy to...
  3. Rozumiesz, że open/close nie jest prawem programowania a jedynie zbiorem wygodnych uproszczeń w komunikacji i nauce
No i teoretycznie teraz wchodząc na poziom drugi myślisz, ze to ostatni poziom i nie odróżniasz tych co jeszcze nie ogarnęli o/c (czy innej prostej zasady) od tych którzy wiedzą, że to już nie działa. Oczywiście to może iść dalej bo skąd wiadomo czy nie ma czwartego poziomu gdzie jednak w jakimś kontekście to działa, a potem piątego który ponownie całą koncepcje odrzuca... Także słowo na koniec to : taki system informatyczny to inżynieria wielu zmiennych a nie zestaw prostych zasad gdzie każdy problem da się rozwiązań skończonym zestawem annotacji i hajbernejtem. Cały czas szukaj nowych źródeł wiedzy i podważaj to co już wiesz.

niedziela, 29 stycznia 2017

Kowariantne Konsekwencje

Mroźnym popołudniem standardowego dnia pracy wiedzeni drzemiącą w nas naturą podróżnika możemy wylądować gdzieś w pośród deklaracji metod bibliotecznych, które w javie8 wielce prawdopodobne będa przyjmować funkcje. I wtedy to naszym oczom może ukazać się coś takiego :

Function<? super V, ? extends T> declarationName

I tak w zasadzie to podobne rzeczy będą się ukazywać nam co chwilę... coraz częściej... w zasadzie w przypadku funkcji inna deklaracja(bez tego super i extends) nie ma sensu. Omówimy sobie skąd się to wzięło, dlaczego tak zostało, czemu inne deklaracje nie mają sensu i dlaczego pomimo, że to jedyna deklaracja z sensem to trzeba z palca zawsze to extends i super dodawać oraz, że istnieje alternatywa którą (znowu) ma w zasadzie wiele języków a nie ma java.

Problemy z typem typu

Dawno dawno temu wszystko w Javie było faktycznie obiektem. Nawet jak nie chciałeś. Jeśli mieliśmy Listę to mieliśmy listę obiektów. Były to czasy gdy monitory CRT wypalały oczy a aplikacja startowała 15 minut po to by zaraz po starcie rzucić wyjątek "class case exception" - no bo trzeba było zgadywać co za typ kryje się za obiektem. Nie było generyków.

Gdy generyki w końcu zawitały do Javy5 te problemy stały się znikome ale fani zbyt uproszczonych rozwiązań dostali nowe tematy do hejtowania bo oto pojawiły się pewne zachowania z punktu widzenia bazującego na "wszystko jest obiektem" - trochę nieintuicyjne.

No bo mamy takie coś, że -> String jest Obiektem -> mamy listę stringów -> List -> lista stringów jest obiektem ale czy stringi w liście są na tyle obiektami, że to także lista obiektów? Okazuje się, że w tym miejscu model się załamuje - bo nie jest!

Gdyby była to dałoby się zrobić coś takiego
List<String> stringi=new LinkedList<>();
List<Object> objjjekty = stringi; 
objjjekty.add(1); //kupa

A ponieważ doprowadziłoby to do eksplozji to takie niecne programowanie jest zabronione. No ale jeśli dobrze nam z oczu patrzy i obiecamy, że nie będziemy tam nic wkładać to kompilator może nam pójść na pewne ustępstwo :

List<? extends Object> probaDruga=stringi;
I jest teraz zabezpieczenie w postaci dziwnego typu co wystarczy by nas powstrzymać przed wrzucaniem tam śmieci

Czy można było inaczej to zrobić? Można było, co zobaczymy później ale patrząc na ten kawałek kodu z listą wydaje się, że mechanizm jest w porządku, nieprzekombnowany i ratuje nas przed ClassCastEx. To był rok 2004. Mija kolejnych 10 lat. Cała dekada. Wychodzi Java8 a w niej Funkcje . I to właśnie ten nowy mechanizm pokaże słabe strony wyborów z przeszłości...

Use Site Variance

Jeśli wejdziesz w definicję java.util.function.Function to zobaczysz do czego te zabawy z generykami doprowadziły

  default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

Pytania czy te "extends" i "super" musza tam być? To co teraz zrobię być może ma jakąś nazwę w nauce o wnioskowaniu (a może nie) ale generalnie one tam być muszą bo... nie ma żadnych argumentów za tym aby w przypadku funkcji ich tam nie było! (ewentualnie ktoś może mnie tutaj do-edukować).

Extends

Jaka jest różnica pomiędzy dwoma poniższymi funkcjami bibliotecznymi? (taka funkcja "map" dla ubogich)

  public static <A,B> Collection<B> libraryMethod(Collection<A> c, Function<A,B> f){
        List<B> l =new ArrayList<>();
        for(A a: c){
            l.add(f.apply(a));
        }

        return l;
    }


    public static <A,B> Collection<B> libraryMethod2(Collection<A> c, Function<? super A,? extends B> f){
        List<B> l =new ArrayList<>();
        for(A a: c){
            l.add(f.apply(a));
        }

        return l;
    }

Różnica pojawi się w wywołaniu bo mając klasę

class User{
    private String name;
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }
}

Chciałbym teraz wykonać poniższe przekształcenie:

Collection<User>  users=new LinkedList<>();
Function<User,String> display=o->o.toString();
Collection<Object> res1=libraryMethod(users,display); // error
Collection<Object> res2=libraryMethod2(users,display);

Ale nie można go wykonać :( Wiem, że w tych akapitach zbytnio się nie rozpisałem ale próbki kodu chyba lepiej pokazują o co chodzi. Bez dodatkowego wysiłku i dodatkowych deklaracji wprowadzamy niczym nie uzasadnione ograniczenie. Nie ma naprawdę żadnego argumentu za ograniczeniem przyjmowanych funkcja do stricte typów biorących udział w równaniu gdyż nic przez to nie zyskujemy a tracimy wszystkie przypadki wywołania gdy typy nie zgadzają się "jeden do jednego". I w zasadzie tak będzie wszędzie - w sensie każdej funkcji bibliotecznej. Nie potrafię znaleźć kontr-przykładu.

Super

Z tym super na początku sprawa jest trudniejsza bo to szalenie nieintuicyjne co tu się dzieje. Starałem się to kiedyś wytłumaczyć tutaj -> http://pawelwlodarski.blogspot.com/2013/08/covariance-i-contravariance-dla.html. Tutaj spróbujmy taki prosty przykład. Niech User ma podklasę jakaś taką specjalną :

class SpecialUser extends User{

    private String somethingSpecial;

    public SpecialUser(String name, Integer age, String somethingSpecial) {
        super(name, age);
        this.somethingSpecial = somethingSpecial;
    }
}

Mamy znowu inna funkcję biblioteczną, która tym razem filtruje nasze obiekty :

 public static <A> Collection<A> filter1(Collection<A> c, Function<A,Boolean> f){
        List<A> l =new ArrayList<>();
        for(A a: c){
            if(f.apply(a)) l.add(a);
        }

        return l;
    }


    public static <A> Collection<A> filter2(Collection<A> c, Function<? super A,Boolean> f){
        List<A> l =new ArrayList<>();
        for(A a: c){
            if(f.apply(a)) l.add(a);
        }

        return l;
    }

I znowu nie ma żadnego powodu by nie pozwolić tej metodzie przyjmować funkcje, które działają na podtypach bo przecież jest to całe is-A zapewnione - w sensie, że w podtypie na pewno znajdą się te metody których wymaga metoda działająca na rodzicu i całość się skompiluje.

Function<User,Boolean> isAdult=user->user.getAge()>= 18;

Collection<SpecialUser>  specialUsers=new LinkedList<>();
// filter1(specialUsers,isAdult); //error
filter2(specialUsers,isAdult);

No i oczywiście jak zajrzysz w implementacje java.util.stream.Stream to i tam też map oraz filter również mają te super i extends -> bo zwyczajnie inna deklaracja nie ma sensu! Ale skoro nie ma sensu to czy dałoby się zrobić to jakoś inaczej by tyle tego nie pisać i by uprościć typy? Oczywiście, gdyż to już jest, to już jest, jest już to
zdrowo i wesoło
we wszystkich językach w koło

Declaration-Site Variance

Otóż w językach innych możemy zamiast pisać po tysiąc razy to "extends" zwyczajnie napisać jeden raz przy deklaracji, domyślnie zawsze będzie "extends", że to jest po prostu natura danej konstrukcji.

C#
interface IProducer<out T> // Covariant
{
    T produce();
}
 
interface IConsumer<in T> // Contravariant
{
    void consume(T t);
}
Kotlin
abstract class Source<out T> {
    abstract fun nextT(): T
}

Scala
trait Function1[-T1,+R] extends AnyRef 

To dlaczego nie tak?

Dlaczego w javie jest to zrobione ala "use-site variance?" . Najprawdopodobniej gdy ten mechanizm powstawał wydawał się doskonałym pomysłem bo java miała jedynie mutowalne kolekcje do których ta maszynka wystarczała. Przypomnijmy - były to lata 2002-2005 -> IE miało 90% udziałów w rynku, ratunku dla branży widziano w tonach xmli a o funkcjach nikt nie myślał. No i w Javie musi być kompatybilność wstecz to jak już zrobili to tak zostało... Jeśli ktoś szuka przykładu długu technicznego to wydaje mi się, że tutaj właśnie go znaleźliśmy.

O nielicznych zaletach i licznej krytyce "Use-Site variance" przeczytacie tutaj -> https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)#Comparing_declaration-site_and_use-site_annotations. Generalnie jeśli chodzi o funkcje to przewaga "declaration-site variance" wydaje mi się bezdyskusyjna. Bardziej problematyczne są kolekcje. O ile chyba Kotlin ma obydwa typy podejść o tyle np. (znowu z tego co mi wiadomo) scala zrezygnowała z "use-site" na jakimś tam początkowym etapie rozwoju gdyż niewiele to dawało a utrudniało mocno koncepcję auto wykrywania typów dzięki któremu programy w scali mają tone mniej tekstu niż to co reprezentuje sobą java. Zaś co do samych kolekcji należałoby przenieść dyskusje na zupełnie inną płaszczyznę zatytułowaną "algorytmy z wykorzystaniem kolekcji niezmiennych" a tu już jesteśmy o krok od FP vs OOP co może nas doprowadzić do ogólnego wniosku, że problemy z generykami w Javie są pochodną wybranego paradygmatu programowania.

Pocieszenie

Jeśli pracujesz w dobrym ogromnym korpo gdzie kastą rządząca są ludzie biegający z plikiem exceli pod pachą (tzw. "jego EXCELencja") to nie masz się czym martwić bo i tak javy8 pewnie nie zobaczysz przez następne 10 lat. Także głowa do góry!

linki


niedziela, 18 grudnia 2016

Sprawność ITSilnika

Za wikipedią : https://pl.wikipedia.org/wiki/Sprawno%C5%9B%C4%87

Sprawność – skalarna bezwymiarowa wielkość fizyczna określająca w jakim stopniu urządzenie, organizm lub proces przekształca energię występującą w jednej postaci w energię w innej postaci, stosunek wartości wielkości wydawanej przez układ do wartości tej samej wielkości dostarczanej do tego samego układ.

Mamy na przykład sprawność silnika cieplnego, do tego z technikum pamiętam, że generator prądu też miał sprawność, a no i w zasadzie wszystko co zamienia jeden rodzaj energii na inny ma sprawność. Zamiana energii programisty w implementację ficzerów też ma swoją sprawność. Zerkniemy trochę na tę sprawność. Trochę na serio ale bardziej nie na poważnie. Bo tak na poważnie to się nie da.

mała uwaga

Jeśli pojawia się słowo "sprawność" to najpierw ostrzeżenie. Niestety branża IT przepełniona jest masą folkloru i samozwańczych metodyk wytwarzania końcowego produktu co owocowało tym, że "sprawność pracy" w przeszłości była określana na podstawie chybionych porównań pomiędzy skrajnie różnymi formami pracy. I tak ludzie, którzy nie za bardzo znali się na wytwarzaniu oprogramowania ale regularnie prasowali swoje koszule określili, że miernikiem sprawności pracy jest ilość wytwarzanych linijek kodu bo przecież jak produkujemy śrubki to im więcej normy ktoś wyrobi to chyba lepiej tak?

To co mam nadzieję zobaczymy poniżej powinno nas przekonać, że jest odwrotnie. Im mniej linii kodu tym lepiej. (z założeniem odpowiedniej czytelności co też trudno zdefiniować bo czasem brak czytelności kodu to nie właściwość kodu ale właściwość niesatysfakcjonującego poziomu wiedzy osoby ten kod czytającej ) . A w zasadzie jak ktoś ma ten współczynnik ujemny czyli np -100 oznacza usunięcie 100 lii kodu i uzyska oczekiwany efekt - to ten ktoś powinien dostać kurde bonus. Każda linia kodu to potencjalne bugi a tu linie kodu poznikały a ficzery są! W zasadzie jeśli programista jest w stanie przyjść do pracy i zrobić ficzera zgodnie z założeniami jakości kodu w 30 minut to powinien iść do domu bo każda minuta obecności programisty przy komputerze zwiększa prawdopodobieństwo pojawienia się bugów.

Na koniec obligatoryjne wspomnienie klasyki : https://en.wikipedia.org/wiki/The_Mythical_Man-Month by przypomnieć, że już od 40 lat niektórzy śmieją się z tworzenia programowania przy pomocy "metodyki ludzkiej fali", o i teraz można przejść do rzeczy prostszych czyli języków programowania.

Sprawność programowania

Jest taki dowcip, że programowanie to konwersja kawy do linii kodu (taki śmieszny śmieszny dowcip). Można to uogólnić na wymianę energii psycho-fizycznej programisty w uporządkowany zestaw cybernetycznych operacji na abstrakcyjnych formach informacji, które to można dla uproszczenia nazwać "ficzerami produktu, które przynoszą hajs".

I tak np. dla uproszczenia rozważmy formę zminimalizowaną tych ficzerów - niech "ficzer" na początku będzie reprezentowany przez minimalną logikę inkrementująca inta. Mamy tu przykład Javy 7 i Javy 8 także zostajemy w mainstreamie.

//Java7
Function<Integer,Integer> inkrementuj7=new Function<Integer, Integer>() {
    @Override
    public Integer apply(Integer integer) {
        return integer+1;
    }
};

//Java8
Function<Integer,Integer> inkrementuj8 = i -> i+1;
W pierwszym przypadku potrzeba więcej ruchów palcami, spalenia większej ilości kalorii i większej aktywności pomiędzy neuronami by wyprodukować rezultat dokładnie taki sam jak jedno-linijkowiec poniżej.

Teraz zerknijmy na bardziej rozbudowany przykład - funkcja map, która robi podobne manipulacje jak analogiczna metoda na Stream'ach - zmienia każdy z elementów według podanej funkcji prostej 'f' -> stąd zagnieżdżone wewnętrzne Function w typie.

Function<List<Integer>,Function<Function<Integer,Integer>,List<Integer>>> map = input -> f -> ...; 

Typ jest dosyć zakręcony a to w zasadzie jedna z najprostszych sygnatur funkcji wyższego rzędu jakie można spotkać. Zobaczmy co możemy uzyskać po wprowadzeniu łatwiejszej notacji typów. Tym razem scala :

val mapFull: Function[List[Integer],Function[Function[Int,Int],List[Integer]]] = input => f => ???
    
val map : List[Integer] => (Int=>Int) => List[Integer] = input => f => ???
Chociaż na początku wydaje się, że różnica to tylko kilka centymetrów to jednak z punktu widzenia programisty różnica w poświęconej energii na kodowanie/dekodowanie typu rozszerzonego jest odczuwalna. Przynajmniej ja ja odczułem. Może drogi czytelniku/czytelniczko przeprowadzić swój własny eksperyment i spróbować popisać samemu/samej te typy.

Ciekawą rzeczą jest tutaj także koszt dekompresji rozwiązania do mózgu programisty i subiektywna ocena tego kosztu wynikająca czasem z braku odpowiedniego doświadczenia. Na przykład :

map(List(1,2,3))(e=>e+1)
map(List(1,2,3))(_+1)
Ten podkreślnik .W scali możemy zastosować taki "plejsholder" zamiast powtarzać wystąpienie argumentu. Dla mnie osobiście jest to trochę czytelniejsze (to z podkreślnikiem) natomiast pamiętam na początku, że nie zawsze było dlatego potrzebny jest pewien czas ekspozycji na dane rozwiązanie by subiektywnie było zakwalifikowane do indywidualnego worka "(według mnie) obiektywnie czytelne"

Dane

Podobnie jak z operacjami na informacjach tak samo można podejść do kosztów energetycznych tworzenia samych informacji. Jaki wqysiłek mierzony w ruchach palcami (a przypomnijmy, że jakieś tam zapalenie nadgarstków to zawodowa choroba programistów) należy ponieść by zdefiniować dane?

Java
class User{
    private Long id;
    private String name;

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}
Kotlin i Scala
//Kotlin zwykła klasa
class User(val id:Int,val name:String)

//Kotlin klasa z equalsami,hashcodami i innymi takimi
data class User(val id:Int,val name:String)

//scala zwykła klasa
class User(val id:Int,val name:String)

//scala - klasa z całym dobrodziejstwem inwetnarza hasz kody,equalsy i pattern matching
case class User(id:Int,name:String)

Oczywiści w 2016 roku nie trzeba każdej literki pisać z osobna bo IDE dużo samo pogeneruje jednakże już w trakcie analizy - nawet mając do czynienia ze znajomymi wzorcami gettery/gettery/gettery - trzeba to gdzieś w pamięci operacyjnej zmagazynować i chyba wtedy zdaje się sód albo jakiś inny pierwiastek jest używany do zmiany potencjału na aksonach (końcówki neuronów) i później jakieś odpadki wyładowań elektrycznych, które po tych wymianach danych pozostały pomiędzy neuronami, muszą być w trakcie snu usunięte.

W javie jest jeszcze ten Lombok. Nie znam produktu jako takiego ale skoro ludzie chcą go używać to strzelam, że ilość mechanicznego kodu, który inaczej musieliby napisać ręcznie jest dla nich bolesna. Coś tam słyszałem, że w Javie 10 może jakas lepsza składnia będzie. Jak będzie to będzie.

Bonus : bezpieczne granice

Ponieważ pojawiła się Scala to być może pojawią sie opinie, że trudny język, że tak dalej i tak dalej... Generalnie o ile przy przejmowaniu kodu z dalekich krain trzeba mieć się na baczności o tyle samemu kontrolując granice można naprawdę traktować scalę jak wygodniejszą jave. I chociaż w środowisku scalowym często sformułowanie "używać scalę jako lepszą javę" często jest używane trochę z negatywnym zabarwieniem, że ktoś niby nie stosuje/rozumie technik niedostępnych w javie (wolne monady 147 poziomu) - o tyle jeśli ktoś zdecyduje się przejść z javy do scali to to jednak od czegoś trzeba zacząć. Można zacząć od prostych rzeczy zgodnie z powiedzeniem "lepszy rydz niż nic".

Pomimo, że scala kojarzy się (przynajmniej mnie) z popularyzacją podejścia funkcyjnego to - być może nie jest wiedza powszechna - ale ale Scala jest bardziej obiektowa niż Java. Bo jak to większość czytanek wspomina - w Javie wszystko jest obiektem.... no może poza klasą która ma same metody statyczne,... no i może poza int... i w sumie też nie long. No i boolean to też nie obiekt.

Weźmy taki standardowy przykład z abstrakcją za interfejsem i fabryką implementacji realizujących tę abstrakcję - możemy to strzelić w scali w kilku linijkach

trait Abstraction{
  def abstractProcess(s:String):String
}

class ConcreteImplementationDupa extends Abstraction{
  override def abstractProcess(s: String): String = s+"dupa"
}

object SurpriseFactoryPattern{
  def create():Abstraction = new ConcreteImplementationDupa
}

I co najważniejsze - jeśli będziemy trzymać się koncepcji, które dla javy 8 nie są czymś obcym to pod spodem dostaniem byte code, który też nie powinien zawierać niespodzianek. I tak trait z abstrakcyjnymi metodami kończy jako zwykły interfejs

public interface jug.lodz.workshops.Abstraction {
  public abstract java.lang.String abstractProcess(java.lang.String);
}

A klasa to zwykła klasa
public class jug.lodz.workshops.ConcreteImplementationDupa implements jug.lodz.workshops.Abstraction {
  public java.lang.String abstractProcess(java.lang.String);
  public jug.lodz.workshops.ConcreteImplementationDupa();
}

Fabryka jako, że stanowi koncept dla javy8 obcy czyli singleton na poziomie języka - jest odwzorowany przy pomocy dwóch klas SurpriseFactoryPattern.class i SurpriseFactoryPattern$.class - czyli jedna jedyna instancja i dostęp do tejże instancji poprzez statyczne metody - w javie jako javie inaczej chyba się nie da

Również jesli chodzi o sygnatury funkcji process kompilacji jest przewidywalny i a na poziomie kodu mamy bardzo czytelne aliasy typów funkcji :

  val map: List[String] => (String=>String) => List[String] = input => f => input.map(f)
Są one przez kompilator zamieniane w znajome dla javy8 SAM type
public static scala.Function1<scala.collection.immutable.List<java.lang.String>,
 scala.Function1<scala.Function1<java.lang.String, java.lang.String>,
 scala.collection.immutable.List<java.lang.String>>> map();

Zakończenie

Na zajęciach z polskiego zawsze była taka teoria, że artykuł ma początek, środek i zakończenie. Gdzie zakończenie to wstęp tylko innymi słowami. Zamiast się tutaj produkować przeczytajcie sobie raz jeszcze wstęp ale co drugie słowo to wyjdzie coś oryginalnego z tym samym przesłaniem i tyle na dzisiaj starczy.

poniedziałek, 21 listopada 2016

Bezpieczny numerek

Wyobrażenie

Za każdym razem gdy spróbujemy wyjść poza najbardziej mainstreamowy język programowania na ziemi musi pojawić się pytanie ale po co. W Javie jest wszystko, frameworki, serwery, narzędzia, są obiekty - a wszystko jest obiektem to chyba jest wszystko?

W tym momencie pojawi się teza. Teza może wynikać z teorii lub obserwacji i ta tutaj wynika właśnie z szeregu obserwacji. Obserwacji, że w 2016 - czyli dwa lata po tym jak Java oficjalnie dostała w podarunku lambdy - wielu programistów javy z kilkuletnim doświadczeniem , a których miale okazję obserwować, miało pewien kłopot w operowaniu tymi "strzałkami". I nie jest to dyskusja typu "bo ja umiem a wy nie" ale raczej czy możne pewne zjawiska ubiec aby być lepiej przygotowanym gdy już nastąpią.

I tak na przykład jeśli planowałbym ogarniać strzałki od pierwszego dnia ich wprowadzenia - czyli któryś tam marzec 2014 - wtedy nie mogę ograniczyć się jedynie do javy bo nie nauczę się mechanizmu, który w tym języku jeszcze przed ta datą nie istnieje... (podobnie w 2004 gdy wyszła java5 z generykami świat javy musiał zaznajomić się z teorią, która wcześniej w nim nie istniała czyli nie możesz przygotować się do rozwoju javy ograniczając się jedynie do javy!)

I tak na przykład widziałem jak doświadczeni programiści C# szybciej łapali Scalę aniżeli ci doświadczeni w Javie7lubmniej. Być może nawet w marcu 2014 taki "uśredniony" programista C# lepiej odnalazłby się w Javie8 aniżeli jego javowy odpowiednik. Do tego kolejną zaleta poszerzenia pola widzenia jest to, że znajomość realizacji tego samego mechanizmu w dwóch lub więcej językach ułatwia zrozumienie abstrakcji, która za tym mechanizmem stoi.

No ale co było to było a za chwilę Java8 będzie miała trzecią rocznicę, lambdy - podobnie jak wcześniej generyki - z roku na rok będą stawać się integralną częścią języka, lada chwila ma być wydana książka "Functional Programming in Java" i istnieją już także wygodne biblioteki "lambdowe" jak Javaslang. W Javie 9 i 10 są zapowiadane pewne udogodnienia (Project_Valhalla) ale na horyzoncie nie widać kolejnej wielkiej rewolucji.

Ale czy na pewno...? (tak z akcentem na 'NA PEwno', i do tego przydałby się jakiś podkład muzyczny - może jakiś wczesny scooter?)

Idzie nowe

The Essence of Dependent Object Types - to tytuł pracy - takiej naukowej - na którą można natrafić o tu --> http://dotty.epfl.ch/#why-dotty a "TU" to strona platformy dotty, która (chyba) ma zdefiniować jak będzie wyglądała Scala 3.0. Jeśli wierzyć autorom mamy do czynienia z zupełnie nowym potężnym podejściem do typów w językach prorgamowania. Niestety z dokumentu za wiele nie rozumiem a samo dotty jest work in progress.

Ale gdy tak siedzę nieco zagubiony nagle, tak znienacka wyłania się język, który w kwestii edukacji może być dla "Typów Zależnych" tym samym czym Haskell dla "Programowania Funkcyjnego " - mowa o Idris. Co ciekawe ten język jest w istocie napisany "na" Haskellu i ma bardzo podobną składnię także w tym przypadku dla mających jakieś pojęcie o Haskellu łatwiej jest zacząć. Tyle gadaniny - zerknijmy trochę pod pokrywkę.

Typ Typu

Cały bajer z typami zależnymi polega na tym, że typ może mieć typ, który może mieć typ itd. Nie będę tutaj grał fachury bo sam hoduje sobie neurony by ogarnąć ten temat ale nawet na najprostszych przykładach widać jak obiecujący jest to mechanizm.

Pierwsza rzecz do której musimy się przyzwyczaić to ta rekurencyjna koncepcja pojęcia typu. Jeśli znowu wrócimy do Javy to przed javą 5 lista to była sucha konstrukcja. Od javy 1.5 możemy już np. mówić, że Lista ma typ String czyli :

List<String>
A teraz... jeśli to pójdzie dalej to ta lista stringów też może mieć typ - dokładnie tak - typ typu - np. List typu String o typie długość cztery
List<String><4>
Oczywiście w Javie nie ma czegoś takiego ale czas na Idris gdzie taka sytuacja to stan naturalny :
fourElements : Vect 4 String
fourElements = ["pierwszy","drugi","trzeci","czwarty"]
Gdybym się pomylił i dał inną ilość elementów to to się nawet nie skompiluje!
fourElements = ["pierwszy","drugi","trzeci"]

No i teraz pierwsze miejsce gdzie ta konstrukcja może sie przydać. Zerknijmy na poznaną już Java8 metoda Stream.map , zmieniająca każdy element według instrukcji podanej w postaci funkcji a->b. W takim javowym pseudokodzie to będzie coś takiego :

List<B> result= List<A>.stream.map(fab).collect(toList);
Problem polega na tym, że mając zabezpieczenie tylko tymi typami a->b bardzo łatwo popełnić błąd. Co gdy zawsze będę zwracać pustą listę? I tak się skompiluje. Podobnie jak analogiczny przykład z Idris z użyciem typu List, która nie ma zależnego typu długości
customStandardMap : (a->b) -> List a -> List b
customStandardMap f [] = []
customStandardMap f (x :: xs) = []
Natomiast gdy przerzucimy się na typ Vect, którego typ ma swój typ dostaniemy taką oto sygnaturę :
customMap : (a -> b ) -> Vect n a -> Vect n b
n to długość kolekcji i przy tak zapisanej funkcji nie ma wyjścia, kolekcja zwracana musi mieć taką samą długość jak ta podana!
-- this will not compile!!!
customMap f [] = []
customMap f (x :: xs) = []

Czas na numerek

Teraz coś z zupełnie innej beczki. Czy można wartość dowolnej liczby naturalnej (jedne, milion,pierdzyliard) zaprezentować przy pomocy jedynie dwóch typów?

Jeśli ktoś spróbował w życiu Scali wie, że tam Lista to nie tyle "kolekcja N elementów" ale po prostu : "element z przodu i (być może) reszta listy". Lista tam jest tzw. typem algebraicznym czyli ściśle ograniczoną rodziną możliwych typów z jasno zdefiniowanymi nań operacjami. A w praktyce Lista to albo ... "Lista" albo "Koniec listy" co pozwala nam wygodnie używać tej konstrukcji rekurencyjnie

safeListIteration: List Integer -> String
safeListIteration [] = ""
safeListIteration (x :: xs) = show x ++":"++ safeListIteration xs

No i teraz jeśli zadziałało dla listy to możemy sobie wyobrazić Kolejne liczby naturalne jako taką listę z końcem ogonka na zerze. I w ten oto sposób dostaniemy bardzo bliźniaczy kod

safeNumberIteration : Nat -> String
safeNumberIteration Z = ""
safeNumberIteration (S k) = show k ++":"++ safeNumberIteration k

Jest to trochę taki mindfuck - dowolna liczba naturalna to : albo zero albo następca innej liczby naturalnej. No i teraz jak tę definicję scalimy z typami zależnymi dostajemy kolejne możliwości zabezpieczenia poprawności programu na etapie kompilacji.

append : String -> Vect n String -> Vect (S n) String
append x [] = [x]
append x xs = x :: xs

Czyli jeśli dodamy element do kolekcji to nie ma ku**a innego wyjścia jak tylko musimy otrzymać kolekcję gdzie długość to kolejna liczba naturalna względem długości kolekcji pierwotnej (być może trzeba to zdanie przeczytać sobie raz jeszcze)

Program zabezpieczony

A teraz taka sytuacja. Jak w Javie byśmy się zabezpieczyli przed np. podaniem indexu przekraczającego długość kolekcji? Albo jak się zabezpieczyć przed podaniem 369 w miejsce gdzie jest spodziewany dzień miesiąca? A jak już się zastanawiasz i widzisz te ify sprawdzające poprawność wartości w rantajmie - to kolejne pytanie - jak to zrobić w trakcie kompilacji?

Weźmy na to ten kalendarz i tylko trzy pierwsze miesiące by było mniej pisania

nToMonth : Fin 3 ->  Month
nToMonth i = case finToInteger i of
                  0 => January
                  1 => February
                  2 => March

Ok co ja pacze? Co to jets ten Fin 3? Otóż jest to zabezpieczenie na poziomie typów - czyli i kompilacji, takie że tam jako argument tejże metody otrzymamy wartość skończoną z zakresu 0-2. Jeszcze raz - TYPÓW - T-Y-P-Ó-W - Typów - types - типы. Jeśli to się skompiluje to znaczy, że działa! Jeszcze raz - jak to się skompiluje to znaczy, ze działa!

No ok ale skąd ten Fin się ma wziąć?. Trzeba pomyśleć o tym w szerszym kontekście. Ta funkcja jest taka czysto biznesowa a funkcja "sito" odpowiedzialna za komunikację z nieprzewidywalnym światem zewnętrznym będzie gdzieś indziej.

firstQuarterMonth : Integer -> Maybe Month
firstQuarterMonth i = case integerToFin i 3 of
                           Nothing => Nothing
                           Just n => Just (nToMonth n)

I teraz ładnie ogarniamy każdą możliwą kombinację wejścia czyli nasz program jako program działa zawsze poprawnie, pomimo iż w trakcie kompilacji nie wiemy co przyjdzie w runtime.

*poligon> firstQuarterMonth 0
Just January : Maybe Month
*poligon> firstQuarterMonth 1
Just February : Maybe Month
*poligon> firstQuarterMonth 2
Just March : Maybe Month
*poligon> firstQuarterMonth 3
Nothing : Maybe Month

Czaisz o co chodzi? Poprawność programu wynika z tego, że zwraca zdefiniowaną odpowiedź na każda możliwa kombinacje wejściową!

Ale czy na pewno?

Oczywiście, że nie bo użyłem tam hamskiego brute forca, żeby na siłę zamienić Fin z powrotem w integer, którego użyłem później w pattern matchingu. Dlaczego tak zrobiłem? Wynika to z prostej przyczyny : inaczej nie umiem. Nauka jest w trakcie trwania i na pewno za kilka miesięcy spojrzę na swój kod w Idris z dzisiaj i być może się porzygam ale to co fajnego wyszło w tym przykładzie, to że tak naprawdę to język zauważył, że odpierdalam a nie ja sam. Idris jest w stanie przeanalizować typy od początku do końca i sprawdzić czy funkcja jest całkowita czyli nei wystąpi jakaś nieprzewidziana kombinacja parametrów wejściowych, która zrobi nam kuku.
*poligon> :total firstQuarterMonth 
Main.firstQuarterMonth is possibly not total due to:
Main.nToMonth, which is possibly not total due to:
Main.case block in nToMonth at poligon.idr:59:19, which is not total as there are missing cases  

Linia 59 to jest dokładnie ta linia z tym brute forcem. Żeby być w stanie zrobić taką analizę programu to jest siła!!! Jeśli to nie wywróci świata IT do góry nogami w przeciągu następnej dekady...



środa, 2 listopada 2016

Wrażenia po Code retreat 2016 którego nie było

W tym roku data Global Day of Code Retreat niestety pokryła się z Mobilizacją dlatego my jako JUG Lodz tegoż wydarzenia nie organizowaliśmy nic mi nie wiadomo by ktoś inny podjął się tego zadania także w Łodzi ćwiczenia tej jesieni chyba się nie odbyły. Cóż - Zobaczymy czy daty za rok dopasują. Ale pomimo braku połączenia video z Koluszkami Górnymi czy innymi miastami wokół globu (oraz brakiem standardowego browara po zajęciach) można samemu przeprowadzić ćwiczenie i wyciągnąć jakąś naukę ze zdarzenia, które się technicznie nie odbyło.

Zasadniczo główną przeszkodą na drodze nauki jesteśmy my sami wyposażeni w bogatą kolekcję błędów poznawczych jak chociażby efekt aureoli, którą sami sobie nakładamy gdy myślimy, że doskonała znajomość jiry i kilkuletnie doświadczenie w odpalaniu tomcata automatycznie oznacza, że ogólnie "umiemy IT bardzo dobrze". "Pisanie kodu, o możliwie niskiej złożoności w wymiarze czasu i przestrzeni" to umiejętność niezależna od znajomości adnotacji w springu, doświadczeniu w konfiguracji mavena czy latach doświadczenia w przekonywaniu, że ten rysunek z 5 kwadratami to kompletny projekt systemu. Także jako umiejętność niezależną należy ją ćwiczyć niezależnie.
Więcej o takich sprawach produkowałem się rok temu : CodeRetreat 2015 i nauka są obrazki z cyklem Kolba (nie mylić z uderzeniem kolbą w skroń, która to może przynieść efekt odwrotny do zamierzonego) jest i teoria atrybucji także można sobie poczytać.

Ograniczenia i przyzwyczajenia

Im dłużej pracujemy w pewien sposób tym bardziej ten sposób dopracowaliśmy do "lokalnej perfekcji" nawet jeśli to jedynie mało wydajne "maksimum lokalne" produktywności. Gdy próbujemy wyjść ze starego sposobu to opuszczamy to maksimum lokalne i nawet jeśli za rogiem czai się potężna "górka efektywności" (ku*wa chyba pójdę w kołczing) to w pierwszym momencie i tak będzie nam szło gorzej.

Stąd też pomysł aby nałożyć sztuczne ograniczenia w trakcie pisania programu, tak abyśmy byli zmuszeni wyjść poza znane nam mechanizmy (ej naprawdę nazwać to "poza cyfrową strefe komfortu", kupić trochę pączków i jeździć po Polsce ze szkoleniami). Oczywiście ograniczenia musza być dobrane w sposób sensowny aby kierować nas w dobra stronę - a skąd wiemy, która jest dobra? CodeRetreat ma pewne gotowe schematy sesji, które czerpią inspirację z doświadczenia ludzie mających ogromne doświadczenie praktyczne. A Skąd wiadomo, że oni mają dobre doświadczenie praktyczne? Tego nie wiadomo nigdy ale taką wskazówką jest podejście tych ludzi do tematu nauki, kiedy to twierdzą iż rozumieją, że w trakcie edukacji trzeba cały czas powracać do etapu początkującego (taka skromność edukacyjna).

Mainstreamowy CodeRetreat jest raczej nakierowany na Javę i dobre praktyki programowania obiektowego - czy można to podejście wykorzystać do nauki programowania funkcyjnego? Może ograniczenie "tylko funkcje", "żadnych przypisań", "nie zmieniaj stanu", "żadnych efektów ubocznych"? No w sumie z myślą o takiej sesji, zostało przygotowane specjalne narzędzie edukacyjne - nazywa się onoHaskell...

Kod

Znam (niestety) wiele opinii i podejść do Haskella - "tego się nie używa to po co się uczyć". A może języka warto się nauczyć dla ... samej nauki? (chociaż tutaj pewnie fani Haskella powiedzą, że w praktyce tu czy tam się go używa). W Haskellu programowaniu funkcyjne jest "natywne" i przez to nauka tego podejścia idzie tam naturalnie i duzo łatwiej niż w Scali czy Javie. No jak na przykład w książce "Functional Programming in Java" jest cały rozdział o tym jak w Javie zasymulować tail recursion, które w Haskellu (a nawet w Scali) po prostu "jest" - to istnieje duże prawdopodobieństwo, że standardowy programista Javy zawróci z tej ścieżki traktując funkcje w Java8 jako taki kosmetyczny dodatek do kolekcji. (to trochę tka jakby naukę programowania zaczynać od lutowania rezystorów do płytki - "Lekcja 3 - nakładamy kalafonię")

Zasady gry Game Of Life, którą to implementuje się na code retreat - opisane są tutaj : https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life. Jak jest tyle a tyle komórek a nasza jest żywa to przezywa albo umiera. I podobnie gdy jest martwa. Czyli dosyć ciekawy zestaw wymagań gdzie występuje stan zarówno wewnątrz komórki jak i poza nią.

No to spróbujmy Haskella i zobaczmy najciekawsze fragmenty, które udało się wygenerować. Na początek w naszym kodzie - komórka. Gra życie polega na ewolucji komórek w kolejnych pokoleniach. Często ludzie słysząc, że komórka ma dwa stany myślą boolean. A Komórka to Komórka. Więc prezentujmy Komórkę jako komórkę.

data Cell = Live | Dead deriving (Show,Eq)
Kolejna rzecz, która jest jasno zdefiniowana w wymaganiach gry to zasady ewolucji komórki w zależności od sąsiadów. Widzimy więc, że komórka jest mocno zależna od stanu zewnętrznego. Trzeba jakoś przekazać tę informację i zazwyczaj tutaj pojawia się jakiś if no bo jest kilka warunków ewolucji komórki. Okazuje się, że mając mechanizmy niespotykane w Javie można te zasady w sposób bardzo czytelny napisać bez użycia ifa - oto mamy pattern matching
next :: Cell -> Int -> Cell
next Dead 3 = Live
next Dead _ = Dead
next Live 2 = Live
next Live 3 = Live
next Live _ = Dead
Następnie przechodzimy do definicji planszy. Generalnie pisząc raz za razem game of life dochodzimy do momentu gdzie w zasadzie liczą się tylko miejsca z żywymi komórkami i możemy łatwo zasymulować nieskończoną przestrzeń przy pomocy mapy. Nie musimy także tworzyć osobnego typu na współrzędne gdyż haskella dostarcza nam wygodne aliasy. W tym przypadku zastosowanie aliasu pomaga łatwo dołożyć na przykład trzeci wymiar gdyż współrzędne działają jedynie jako "klucz" i kawałki kodu świadome ilości wymiarów są ograniczone i łatwe do ogarnięcia.
type Coordinates = (Int,Int)
type Board = M.Map Coordinates Cell

initial:: [Coordinates]
initial = [(0,0),(0,1),(1,1),(2,1)]

initialBoard ::M.Map Coordinates Cell
initialBoard = M.fromList $ map (\c->(c,Live)) initial

Tu macz comprehenszyn

W jaki sposób znaleźć współrzędne wszystkich sąsiadów? Szukamy iloczynu kartezjańskiego sasiadów w osi x z sąsiadami w osi y. W Javie można to zrobić dwoma zagnieżdżonymi forami - no chyba, że akurat jest ograniczenie "bez zagnieżdżeń" - wtedy robi się na przykład klasy Range, Surroundings i jakiś Carthesian (Function,Service co tam chceta). A w Haskellu ?
neighbours:: Coordinates -> [Coordinates]
neighbours (x,y) = [(xn,yn) |xn <- [x-1..x+1],yn<-[y-1..y+1], (xn,yn)/=(x,y) ]
No proszę tylko jedna linijka! Ale to oszukiwanie bo tam są poukrywane monady... W każdym razie podobnie możemy policzyć żywych sąsiadów
liveNeighbours :: Coordinates -> Board -> Int
liveNeighbours c board = sum [1 | coord <- neighbours c,M.member coord board]
Ponieważ zyskujemy tutaj dodatkowy wymiar kompozycji pod nazwą currying czyli innymi słowy możemy niezależnie zaaplikować Board i Coordinate, więc powstaje pytanie czy obecna kolejność jest właściwa : Coordinates -> Board -> Int czy może powinno być na odwrót czyli Board-> Coordinates -> Int? O tym, że problem nie jest banalny starłem się napisać tutaj : http://pawelwlodarski.blogspot.com/2016/03/o-rany-odwrotna-kolejnosc-danych.html natomiast w tymże ćwiczeniu odpowiedzi nie znam. W tej formie w jakiej jest kod kolejność wydaje się nie mieć znaczenia bo funkcja i tak ani nie weźmie udziału w kompozycji ani nie będzie niezależnie aplikować argumentów.

Końcówka

Końcówkę umieszczam w zasadzie tylko aby za rok zrobić porównanie. Generalnie metody wydają się krótkie ale jest to złudne gdyż te kilka linijek w Javie7 zajęłoby linijek kilkadziesiąt. Logika jest dosyć "skompaktowana" i wymaga od umysłu programisty odpowiedniej mocy obliczeniowej na rozpakowanie.

potentialCells :: [Coordinates] -> [Coordinates]
potentialCells cs = [(xn,yn) | c<-cs,(xn,yn)<-neighbours c, xn>=0 , yn>=0]

nextStep :: Board -> Board
nextStep board =M.filter ( == Live) $  M.fromList $ map mapCell newCells
  where
    newCells = potentialCells $ M.keys board
    mapCell cord =(cord, next (M.findWithDefault Dead cord board) (liveNeighbours cord board))

Odpalenie też w sumie wykorzysta kilka ciekawych mechanizmów :
map (map fst  . Data.Map.toList) $ take 5 $  iterate nextStep initialBoard
    -- [[(0,0),(0,1),(1,1),(2,1)],[(0,0),(0,1),(1,1),(1,2)],[(0,0),(0,1),(0,2),(1,0),(1,1),(1,2)],
[(0,0),(0,2),(1,0),(1,2),(2,1)],[(1,0),(1,2),(2,1)]]
W wyniku dostajemy kolejne fazy gry i chyba są nawet poprawne - niestety jeszcze nie ogarnąłem tematu testów jednostkowych w Haskellu także dowód jest empiryczny. Podejście było dosyć restrykcyjne z punktu widzenia Obiektówki bo nie mamy żadnego zmiennego stanu. Natomiast widać cały czas pewne oznaki takiego trochę proceduralnego programowania gdzie kawałki kodu niejako nie komponują się a jedynie następują jeden po drugim.
Także by zabawy było więcej, czas dorzucić jakieś ograniczenia w ramach już istniejących ograniczeń.

Ograniczenia

Pojawiają się pewne rzeczy, które potencjalnie nadają się na ograniczenia
  • maks jeden $ w linii - generalnie każdy kolejny '$' można traktować jako kolejną linie kodu tyle że... w poziomie.
  • brak zagnieżdzonych nawiasów "))" - kolejny sposób na wywołanie instrukcji w instrukcji
  • brak where,let i list comprehension - tak by nie zamykać logiki w lokalnych aliasach
Kod zaczyna się podobnie, w sensie operacje na komórkach sa jasno określone przy pomocy pattern matchingu. Ciekawie zaczyna robić się przy określaniu współrzędnych. Na początek prosty jednowymiarowy generator sąsiadów z uwzględnieniem krawędzi planszy co być może jest niepotrzebne bo przecież poruszamy się po planszy nieskończonej.
surrounding :: Int -> [Int]
surrounding i = filter (>=0) [i-1,i,i+1]
I teraz zaczyna się robić naprawdę ciekawie bo aby zdefiniować współrzędne sąsiadów potrzebne jest połączenie "każdy-z-każdym". Tyle, że bez comprehension i zagnieżdżonych funkcji. Jak to zrobić?
wchodzimy na hoogle (https://www.haskell.org/hoogle/) i szukamy czegoś co połączy nam dwie listy w listę tupli, które są u nas współrzędnymi. [a]->[a]->[(a,a)]. Znajdujemy funkcję zip i jakieś inne funkcje specjalizowane. Ponieważ mamy REPLa (od wersji 9 w końcu ma być repl w Javie!) możemy sobie szybko zrobić test
zip [1,2] [3,4]
 >> [(1,3),(2,4)]
To nie to o co nam chodziło. Spróbujmy poszerzyć poszukiwania. Może znajdziemy coś co łączy dwie listy dając nam do dyspozycji mechanizm połączenia ich w wygodny dla nas sposób (a->b->c)->[a]->[b]->[c] . I tu pojawi się coś ciekawego na miejscu drugim :
  1. zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
  2. liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
Applicative w tej sygnaturze to w wolnym tłumaczeniu "Klasa Typu", która znowu w wolnym wytłumaczenia trochę po javowemu mówi, że przekazany typ musi spełniać określony interfejs. Czy lista go spełnia?
:info []
data [] a = [] | a : [a]  -- Defined in ‘GHC.Types’
instance Eq a => Eq [a] -- Defined in ‘GHC.Classes’
instance Monad [] -- Defined in ‘GHC.Base’
instance Functor [] -- Defined in ‘GHC.Base’
instance Ord a => Ord [a] -- Defined in ‘GHC.Classes’
instance Read a => Read [a] -- Defined in ‘GHC.Read’
instance Show a => Show [a] -- Defined in ‘GHC.Show’
instance Applicative [] -- Defined in ‘GHC.Base’      `` <- spełnia!spełnia! ojjjj spełnia!
...
Ale skąd weźmiemy funkcję? To jest dosyć ciekawa perspektywa poznawcza. Struktura z która pracujemy w zasadzie... jest funkcją
:t (,)
>> (,) :: a -> b -> (a, b)
i eksperyment :
Control.Applicative.liftA2 (,) [1,2] [3,4]
>> [(1,3),(1,4),(2,3),(2,4)]
Control.Applicative.liftA2 (,) [0,1,2] [1,2]
>> [(0,1),(0,2),(1,1),(1,2),(2,1),(2,2)]

Ćwiczenie można kontynuować bez wnikania w to co to jest faktycznie to Applicative. Temat jest gruby i w dużym skrócie można napisać, że Applicative umożliwia zastosowanie prostej funkcji a->b->c czyli u nas a->b->(a,b) na poziomie złozonychefektów. Tylko jakich efektów i w jakim sensie złożonych? Czego efektem jest lista? Jeśli założymy, że funkcja może mieć tylko jeden wynik to zwrócenie listy, która jest "wieloma wynikami", oznacza brak determinizmu. No bo jeśli mamy funkcję MONETA->STRONA i wyniku dostajemy [ORZEL,RESZKA] to po dwóch wywołaniach mamy [ORZEL,RESZKA] i [ORZEL,RESZKA] i łącząc ze sobą dwa niedeterministyczne wyniki otrzymujemy wszystkie możliwe kombinacje - co też zobaczyliśmy na przykładzie szukanie sąsiadów.

I to w zasadzie tyle.
Tyle?!? Ale przecież nieskończone!!!
Nie musi być skończone a nawet nie powinno dlatego sesje na code retreat trwają 45 minut i nie zakładają, że ktoś skończy. I aby nie przywiązywać się do jednego rozwiązania kasujemy kod po sesji i to też własnie robię.

Podsumowanie

W ferworze implementacji wyszło mi coś takiego map (\(coord,cell)->(coord,next cell)) na co haskell-ide zareagowała "po co się meczysz z czymś takim zamiast użyć Control.Arrow.second"

:t Control.Arrow.second 
Control.Arrow.second :: Arrow a => a b c -> a (d, b) (d, c)
Jak już myślałem, że zaczynam ogarniać te monady to teraz zerkłem se na to i : "Arrows, like monads, express computations that happen within a context. However, they are a more general abstraction than monads, and thus allow for contexts beyond what the Monad class makes possible". Także ten tego... nauka nie kończy się nigdy...