środa, 22 czerwca 2016

Prymitywna szybkość i lekkość

W Java8 są takie wynalazki jak interfejs ObjIntConsumer a nie ma na przykład funkcji/metody zip. Metoda/funkcja zip (takie czasy, ze już nie wiadomo czy funkcja czy metoda) nie zwraca kodu pocztowego ale w teorii łączyłaby dwa strumienie/kolekcje i jest ona o tyle ciekawa, że np. w takich tutorialach do Haskella pojawia się co krok a szczególnie wygląda fantastycznie w niezwykle praktycznym przykładzie implementacji ciągu Fibonacciego na strumieniach (naprawdę jest fajne - nie ma stackoverflow i w ogóle.)

Brak zipa to (podobno) jedna z konsekwencji tego, że mamy Stream,IntStream,DoubleStream,LongStream - po co to i co to daje? Ano coś daje -> patrzymy dalej ->

Patrz stary jakie pomiary

Mam dwa takie chytrze spreparowane kawałki kodu aby pokazać co dają "prymitywne strumienie" w Java8. Pierwszy kawałek kodu jest przeznaczony dla "wszystko jest obiektem" lajfstajlu :

long numberOfElements = 1000000000;

Long result = Stream
             .iterate(0L, i -> i + 1)
              .map(i->i*i)
              .limit(numberOfElements)
              .reduce(0L,Long::sum);

Drugi zaś kawałek ma za zadanie pokazać jak na pierdyliard sposobów w javie można zdefiniować funkcje która dodaje jeden (w zależności od typu, fazy księżyca i aktualnej pozycji reprezentacji Polski w grupie)

long numberOfElements = 1000000000;

LongUnaryOperator step = i -> i + 1;
LongUnaryOperator intSquare = i -> i * i;
LongBinaryOperator sum = (i1, i2) -> i1 + i2;

long primitiveResult = LongStream
            .iterate(0, step)
            .map(intSquare)
            .limit(numberOfElements)
            .reduce(0, sum);

Dalsza część zabawy wygląda podobnie jak laborki w technikum - "wiemy, że prąd działa jak prąd bo mamyw domu pralkę i telewizję ale zrobimy pomiar by to potwierdzić. A jak się nie będzie zgadzało to będziemy robić do skutku".

Ogólnie pomiary wydajności to dosyć delikatna sprawa bo niby każdy powinien to umieć robić ale zazwyczaj przy większości prezentacji o "Performensie" czy jakimś tam tutorialu zazwyczaj jest teza, że większośc ludzi robi to źle. Sam robiłem to źle kiedyś bo kiedy skończyłem studia ze średnią 4coś i zerowa wiedzą jak działa JVM to brałem po prostu System.miliseconds a później klasyk "end minus start".

Później po odkryciu, że jest coś takiego jak JIT, kod Javy w sumie nie do końca jest interpretowany a suchary o tym, że java jest wolna są powielane przez programistów c++ , którzy swoja tożsamość budują nad ilością kontroli, którą sprawują nad kawałkiem pamięci - po tym wszystkim okazało się, że w sumie trudno stwierdzić czy pomiary wydajność kodu javy zrobiło się dobrze bo nie wiadomo jak ma się to co jest napisane do tego co chodzi tam w rantaimie.

(Dodatkowo dobiło mnie kiedyś jak przeczytałem gdzieś, że większość pomiarów czasu odpowiedzi strony www mierzy w dużej mierze jaka działa infrastruktura interentu a nie jak działa strona www ale już nie pamiętam i nie moge tego znaleźć teraz)

No ale teraz jest ratunek, jest narzedzie - jest nadzieja!

jmh - teraz i ty możesz mówić, że umiesz robić pomiary!

Wydaje mi się, że najlepiej wejść tutaj : http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/ i pooglądać sobie przykłady. Ja na początku trafiłem na tutorial z 2013 gdzie jakiś typ opisywał krok po kroku jak to konfigurować. Ponieważ otrzymywałem w wyniku czas wykonania 0 to na początku popadłem w depresję, że nawet jednej adnotacji nie umiem poprawnie dodać do kodu.

Na szczęście okazało się, że bezmyślnie kopiując kod używałem wersji 0.1 jmh. Po dodaniu zależności do 1.12 ku memu zachwytowi zaczęło to działać. A działać to oznacza, że udało się odpalić coś takiego :

@Benchmark
@BenchmarkMode(Mode.AverageTime)
public void boxed(){
   long numberOfElements = 1000000000;

   Long result = Stream
                .iterate(0L, i -> i + 1)
                .map(i->i*i)
                .limit(numberOfElements)
                .reduce(0L,Long::sum);


    System.out.println(result);
}

I dało końcu długo oczekiwany wynik :

# JMH 1.12 (released 81 days ago)
# VM version: JDK 1.8.0_91, VM 25.91-b14
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op

# Run progress: 0.00% complete, ETA 00:00:40
# Fork: 1 of 1
# Warmup Iteration   1: 3338615082255021824
13.917 s/op
# Warmup Iteration   2: 3338615082255021824
13.359 s/op
(...)
Iteration   1: 3338615082255021824
11.543 s/op
Iteration   2: 3338615082255021824
11.345 s/op
Iteration   3: 3338615082255021824
11.068 s/op
Iteration   4: 3338615082255021824
10.899 s/op
Iteration   5: 3338615082255021824
13.932 s/op
Iteration   6: 3338615082255021824
13.719 s/op
Iteration   7: 3338615082255021824
13.830 s/op
(...)

Result "boxed":
  13.295 ±(99.9%) 0.937 s/op [Average]
  (min, avg, max) = (10.899, 13.295, 14.231), stdev = 1.080
  CI (99.9%): [12.358, 14.233] (assumes normal distribution)


# Run complete. Total time: 00:08:42

Benchmark      Mode  Cnt   Score   Error  Units
JMHTest.boxed  avgt   20  13.295 ± 0.937   s/op

Jak komuś nie chce się czytać to wyszło średnio : 13 sekund

Po wsadzeniu kodu dla strumienia Longów :

Result "primitive":
  1.580 ±(99.9%) 0.009 s/op [Average]
  (min, avg, max) = (1.568, 1.580, 1.601), stdev = 0.011
  CI (99.9%): [1.571, 1.589] (assumes normal distribution)


# Run complete. Total time: 00:01:04

Benchmark          Mode  Cnt  Score   Error  Units
JMHTest.primitive  avgt   20  1.580 ± 0.009   s/op
Wyszło z 10 razy szybciej. Pytanie czy te pomiary są poprawne? Przede wszystkim nie zastosowałem się do zalecenia ze strony, żeby nie puszczać tego z IDE no i trudno mi stwierdzić czy ten kod jest na tyle "chytry", że JIT nic tam nie pousuwa. Wygląda na to, ze jmh jest przynajmniej wystarczająco sprytne by to jakoś odpowiednio odpalać tak, że iteracje rozgrzewania nie różnią się jakoś specjalnie czasem od iteracji wykonania to chyba ten czas 13s i 1.5s jest już po optymalizacji. Chyba. Temat jest ciekawy i wart zbadania.

I jeszcze sterta

Aby wpis był ciekawszy potrzebne są jakieś obrazki.

Tak wyglądał wykres zajętości pamięci pokazany przez visualvm dla przykładu ze strumieniem Łobiektów. A poniżej przykład dla strumienia prymitywnych Longów. Warto zauważyć, że operacje na prymitywach nie wywołały nawet pierdnięcia na procku.

Jak wyglądał pomiar? Zapauzowałem program na wczytywaniu znaku, odpaliłem visualvm i dalej program. Czy to jest dobrze? Nie wiem. Wykres na sprawozdanie wyszedł zgodny z przewidywaniami teoretycznymi.

Dlaczego nie ma zipa?

Jest gdzieś na necie prezentacja gościa z Oracla gdzie tłumaczy, że gdyby chcieli zrobić zipa dla wszystkich kombinacji różnych strumieni to wyszłoby tego z kilkanaście różnych wariacji. A gdyby chcieli dorzucić coś jeszcze to ilość wymknęła by się spod kontroli. Niby wszystkie te strumienie dziedziczą z czegoś co nazywa się BaseStream ale widocznie nie jest to takie abstrakcyjne aby można było napisać zip(BaseStream s1,BaseStream s2)

No i te 4 funkcje - chociaż wyglądają identycznie - to z punktu widzenia javy w kontekście typu mają ze sobą wspólnego NIC.

UnaryOperator<Integer> f1=i->i+1;
IntUnaryOperator f2=i->i+1;
LongUnaryOperator f3=i->i+1;
DoubleUnaryOperator f4=i->i+1;

Inne

Myślałem, że trochę przykozaczę i pokażę alternatywę bo w Scali są takie adnotacje

trait Function1[@specialized(scala.Int, scala.Long, scala.Float, scala.Double) -T1
I to według sprawi, że kompilator wygeneruje prymitywne wersje funkcji tak jak trzeba czyli gdy mamy :
class Specialization{
  val f:Function[Int,Int]=i=>i+1
}
To po dekompilacji faktycznie używa prymitywnych intów :
public final class mails.Specialization$$anonfun$1 extends scala.runtime.AbstractFunction1$mcII$sp implements scala.Serializable {
  public static final long serialVersionUID;
  public final int apply(int);
  public int apply$mcII$sp(int);
  public final java.lang.Object apply(java.lang.Object);
  public mails.Specialization$$anonfun$1(mails.Specialization);
}

Tej adnotacji nie ma w klasie Stream toteż w sumie tak trochę niepewny rezultatu odpaliłem ten kod :
val result=Stream
      .iterate(0L)(_+1)
      .map(i=>i*i)
      .take(1000000000)
      .reduce(_+_)

    println(result)
I musiałem przerwać bo youtube mi się zaczął przycinać. Temat ciekawy i do dalszego zgłębienia - być może ja robię coś źle (pewnie tak) a być może scala 2.12 która ma chodzić już na Javie 8 gdzieś tam pod spodem te IntStreamy ładnie wykorzysta i w przyszłości "samo z siebie" zadziała.

Próbowałem też z innym mniej popularnym językiem :

execute :: Int
execute = sum $ take 1000000000 $ map (+1) [1..]
Ale to już tak zagrzało kompa, że resztę testów zostawię na zimę...

Warsztat dla początkujących

Generalnie brak znajomości tego co się dzieje "pod spodem" trochę utrudnia programowanie. Także w duchu wspólnej edukacji meetup 5 lipca

Warsztaty Java 8 Streams - Wstęp - 5 lipca

Podsumowanie

To jest miejsce na jakieś mądre podsumowanie - jak mi cos kiedyś przyjdzie do głowy to może uzupełnię.

środa, 8 czerwca 2016

Dziwna metoda reduce

Głównym językiem tego posta będzie Java8 której pisanie zużywa klawiaturę jak mało który język.. Ale będzie też o tzw. "sztuce upadania". Był o tym jakiś czas temu artykuł w magazynie Coaching - numer listopad-grudzień 2015. Konkretnie artykuł nazywa się "Dlaczego warto ćwiczyć upadanie" i w formie wywiadu opowiada o toksycznej zdaniem autorów (i nie tylko) obecnej kulturze, gdzie każdy się musi porównywać z człowiekiem obok, zawsze ale to zawsze musi się wszystko udawać a jak się nie udaje to jest tragedyja nad tragedyje.

No i przez to ludzie się boją eksperymentować i tkwią w czymś popularnie zwanym "strefa komfortu". Później w artykule jest porównanie do sztuk walki gdzie ćwiczy się pady i oswaja z sytuacją "gleby", że ta już nie jest straszna itd itp. I właśnie w tym duchu będzie ten post gdzie nie wszystko będzie działało. A w zasadzie cały czas nie będzie działało i dopiero cos pod koniec zadziała.

Ale od poczatku...

Gdzie jest mój Fold????

W Scali i nie tylko mamy na kolekcjach kilka metod, które potrafią zamienić kolekcję w "jedną rzecz". Ta "jedna rzecz" to może być intem jak np. sumujemy cyfry w liście lub nawet i inna kolekcja jeśli chcemy elementy przekształcić i (lub) przepisać. Metody nazywają się "fold,foldRight,foldLeft" i wyglądają mniej więcej tak :

 def foldLeft[B](z: B)(op: (B, A) => B): B 

Mamy element zerowy oraz przepis na doklejanie kolejnych kawałków .Elementy to A zaś element zerowy to B także nie muszą mieć ze sobą za wiele wspólnego. Metoda jest na tyle potężna i uniwersalna, ze można przy jej pomocy zaimplementować większość możliwych operacji na kolekcjach i nawet ma swoją stronę an wikipedii : https://en.wikipedia.org/wiki/Fold_(higher-order_function)

Czy ktoś z was ma swoją stronę na wikipedii? No właśnie...

No i w Javie8 tego nima (albo ja nie mogę tego znaleźć). Za to jest coś innego. W sensie inna operacja i pomimo, że wygląda dziwnie to niesie ze sobą coś równie silnego.

Coś innego

Pierwszy wariant to reduce z tzw. BinaryOperatorem, który jest funkcją (A,A)=>A tyle, że brzmi bardziej obiektowo i zwiększa pule typów dodanych do Javy8

Optional<T> reduce(BinaryOperator<T> accumulator);
Zwraca Optional gdyż w przypadku pustego Streama nic tam nie ma (czyżby rym?).

Druga opcja dostarcza element domyślny nazwany identity i już nie potrzeba Optionala bo wiemy, ze tam coś zawsze jest.

T reduce(T identity, BinaryOperator<T> accumulator);
To co jest warte uwagi to to, że tam je ino tylko jeden typ T.

No i na pewno teraz będzie jakaś sygnatura funkcji z identity o innym typie. No ma sens by tak było.Ma to sens .. ale nima. No prawie nima bo od razu jedziemy z czymś takim.

<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);
To nie jest fold. To jest bardziej skomplikowane. Ale tam dzieje się coś ciekawego.

Nauka w szeroko pojętym znaczeniu

Wymiar ciekawości rozciąga się poza Javę 8 gdyż teraz na Courserze trwa kurs o programowaniu równoległym w Scali - tak w Scali - i tam tę sygnaturę wyprowadzili krok po kroku. Oczywiście na normalnych funkcjach bez wynalazków "BinaryOperator" czy "BiFunction". Ale to jest znowu taka uwaga aby edukację skalować też w szerz a nie tylko wzwyż - czyli zamiast maksymalnego ubijania jednej tylko technologi (albo nawet i kawałka technologi) tylko dlatego, że akurat "w tym robię an co dzień", poczytać trochę rzeczy pobocznych. Cały czas uważam, że jak kto chce lepiej zrozumieć naturę funkcji w Javie to powinien pouczyć się trochę Haskella bo tam te mechanizmy są naturalne.

Bo młodość jest po to by eksperymentować

Od dzieciństwa bardzo marzyłem by zebrać w jednej kolekcji 10 kolejnych liczb parzystych poczynając od zera. I dzisiaj to zrobimy :

        List<Integer> even = new LinkedList<>();


        List<Integer> result1 = Stream
                .iterate(0, e -> e + 1)
                .filter(e -> e % 2 == 0)
                .limit(10)
                .reduce(even,
                        (partial, elem) -> {
                            partial.add(elem);
                            return partial;
                        },
                        (partial1, partial2) -> {
                            partial1.addAll(partial2);
                            return partial1;
                        }
                );

Generalnie obliczenia równoległe tego typu nie mają żadnego sensu bo narzut samej maszynerii będzie dużo większy niż gdyby jeden procek to szybko policzył ale po raz kolejny podkreślimy role edukacyjną tego problemu (także podkreślamy). Metoda reduce przyjmuje trzy argumenty :

  • Element początkowy - tutaj przekazujemy standardową Listę z Javy, która nazywa się "mutowalna" chociaż pewnie takiego słowa w języku polskim nie ma. Programowanie równoległe i kolekcja mutowalna - no co może pójść źle?
  • przepis jak do kolekcji dokładać nowe elementy - pewnie te kawałki pójdą równolegle
  • przepis jak połączyć rezultaty cząstkowe

No i na pierwszy rzut oka działa :

System.out.println(result1);

//[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Ale działa tylko dlatego, że źle tego używam. No i pytanie jaka jest tam rola pierwszego parametru skoro i tak dostajemy rezultat z całego działania. Wszystko się wyjaśni kiedy zaczniemy tego mechanizmu używać poprawnie i dostaniemy błąd.

Równoległe

Poniższy rysunek przedstawia jak "mi się wydawało, że to działa". I nawet jeszcze dzisiaj zobaczymy jak przy pomocy pewnej listy reduce tak własnie zadziała. Ale to będzie specjalna lista a nawet nie lista tylko Wektor a w sumie Vector ale nie ten z Javy tylko inny. A ta teraz to standardowa lista - taka mutowalna - wrzucona między kilka wątków - czeka nas rzeź...

Spróbujmy niezależnie zdefiniować dwie funkcje używany w poprzednim przykładzie :

        Function<List<Integer>, Integer, List<Integer>> accumulator = (partial, elem) -> {
            System.out.println("accumulator : " + Thread.currentThread().getName());
            partial.add(elem);
            return partial;
        };

        BinaryOperator<List<Integer>> combiner = (l1, l2) -> {
            System.out.println("combiner : " + Thread.currentThread().getName());
            l1.addAll(l2);
            return l1;
        };
Dodamy informacje o wątku w którym działają oraz dokonamy obserwacji, że Lista w Javie jest dziwna bo operacje nań wykonane wcale nic nie zwracają i przez to trzeba dodawać dodatkową linię z return. Oczywiście - tutaj mały spoiler - ta obserwacja pojawiła się gdyż cały czas źle tego wszystkiego używamy. Zaraz dojdzie do nas co i jak.

I teraz taki kod:

        List<Integer> even2 = new LinkedList<>();
        List<Integer> result2 = Stream
                .iterate(0, e -> e + 1)
                .filter(e -> e % 2 == 0)
                .limit(10)
                .reduce(even2, accumulator,combiner);


        System.out.println("result2 : "+result2);
        System.out.println("even2 : "+even2);
Generuje taki rezultat :
accumulator : main
accumulator : main
accumulator : main
accumulator : main
accumulator : main
accumulator : main
accumulator : main
accumulator : main
accumulator : main
accumulator : main
result2 : [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
even2 : [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Czyli generalnie wszystko odbywa się w jednym wątku i combiner w ogóle nie jest użyty.

Można to łatwo naprawić dając "parallel" w ciąg wywołań strumienia :

 .limit(10)
 .parallel()
 .reduce(even2, accumulator,combiner);
No i naprawiliśmy problem z ilością wątków i wywołaniem "combinera" :
accumulator : ForkJoinPool.commonPool-worker-3
accumulator : ForkJoinPool.commonPool-worker-4
accumulator : ForkJoinPool.commonPool-worker-6
accumulator : ForkJoinPool.commonPool-worker-5
accumulator : main
accumulator : ForkJoinPool.commonPool-worker-1
accumulator : ForkJoinPool.commonPool-worker-6
accumulator : ForkJoinPool.commonPool-worker-2
accumulator : ForkJoinPool.commonPool-worker-4
accumulator : ForkJoinPool.commonPool-worker-3
combiner : ForkJoinPool.commonPool-worker-4
combiner : ForkJoinPool.commonPool-worker-2
combiner : ForkJoinPool.commonPool-worker-1
combiner : ForkJoinPool.commonPool-worker-2
combiner : ForkJoinPool.commonPool-worker-4
combiner : ForkJoinPool.commonPool-worker-3
combiner : ForkJoinPool.commonPool-worker-2
combiner : ForkJoinPool.commonPool-worker-3
I przy okazji popsuliśmy wszystko inne :
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:598)
    at java.util.concurrent.ForkJoinTask.reportException(ForkJoinTask.java:677)
    at java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:735)
    at java.util.stream.ReduceOps$ReduceOp.evaluateParallel(ReduceOps.java:714)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
    at java.util.stream.ReferencePipeline.reduce(ReferencePipeline.java:484)
(...)   
Caused by: java.lang.ArrayIndexOutOfBoundsException: 240
    at java.util.LinkedList.toArray(LinkedList.java:1053)
    at java.util.LinkedList.addAll(LinkedList.java:408)
Jest ReduceOps, AbstractPipeline i kilka innych rzeczy, których przeznaczenia można się jedynie domyślać. Tak czy inaczej nie działa.

To jest ten moment kiedy zaczynasz się zastanawiać : "A może przed włączeniem trzeba było przeczytać dokumentację?". Dokumentacja : https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html w zasadzie ma wydzieloną sekcję Mutable reduction i jasno opisuje, że reduce nie jest do tego. Do rzeczy mutowalnych jest collect a reduce do niemutowalnych. Zaraz przejdziemy do collect ale wcześniej jeszcze jeden eksperyment.

Persystentna Struktura Danych

(słowa persystentna też pewnie nie ma w słowniku)

Użyjemy Vector gdyż podobno twórca Javaslang brał inspiracje ze Scali a w Scali Vector jest zaimplementowany jako drzewo i czasem operacje działają na nim szybciej niż na liście... ale czasem wolniej... generalnie trzeba zawsze mierzyć ... dobra tutaj używamy Vectora by było bardziej kolorowo a nie tylko Lista i Lista

import javaslang.collection.Vector;
Vector<Integer> even = Vector.empty();

Od razu nasze funkcje się uproszczą gdyż operacje na "trwałych" (może to jest to słowo) strukturach danych zwracają nowy rezultat bo inaczej nie miałoby to żadnego sensu.

       BiFunction<Vector<Integer>,Integer,Vector<Integer>> accumulator= (partial, elem)->{
            System.out.println("accumulator : "+Thread.currentThread().getName());
            return partial.append(elem);
        };

        BinaryOperator<Vector<Integer>> combiner=(l1, l2)->{
            System.out.println("combiner : "+Thread.currentThread().getName());
            return l1.appendAll(l2);
        };

I teraz tak napisany kod :

       Vector<Integer> result = Stream
                .iterate(0, e -> e + 1)
                .parallel()
                .filter(e -> e % 2 == 0)
                .limit(10)
                .reduce(even, accumulator,combiner);


        System.out.println("even : "+even);
        System.out.println("result  : "+result);
Działa poprawnie - nie wiadomo czy szybko - ale poprawnie.
accumulator : ForkJoinPool.commonPool-worker-5
accumulator : ForkJoinPool.commonPool-worker-2
accumulator : ForkJoinPool.commonPool-worker-4
accumulator : ForkJoinPool.commonPool-worker-7
accumulator : ForkJoinPool.commonPool-worker-6
accumulator : main
accumulator : ForkJoinPool.commonPool-worker-3
accumulator : ForkJoinPool.commonPool-worker-1
accumulator : ForkJoinPool.commonPool-worker-4
combiner : ForkJoinPool.commonPool-worker-7
combiner : ForkJoinPool.commonPool-worker-2
accumulator : ForkJoinPool.commonPool-worker-5
combiner : ForkJoinPool.commonPool-worker-1
combiner : ForkJoinPool.commonPool-worker-5
combiner : ForkJoinPool.commonPool-worker-7
combiner : ForkJoinPool.commonPool-worker-5
combiner : ForkJoinPool.commonPool-worker-5
combiner : ForkJoinPool.commonPool-worker-1
combiner : ForkJoinPool.commonPool-worker-1
even : Vector()
result  : Vector(0, 2, 4, 6, 8, 10, 12, 14, 16, 18)

Ten przykład był po to aby czytelnik zdobył lepszą intuicję w rozumieniu różnic pomiędzy "trwałymi" i "nietrwałymi" (tak to an pewno te słowa!) strukturami danych oraz, że trwałe/niemutowalne nie musi koniecznie oznaczać inta - a można tak pomyśleć bo wiele przykładów na necie liczy sumę, zawsze sumę czegoś.

collect

Metoda collect wspomniana w dokumentacji wygląda tak :

   <R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);
Supplier i consumer to klasa tzw. "funkcji dziwnych". Pierwsza tworzy coś z niczego a druga nic z czegoś czyli po prostu połyka argument i generuje jakiś "efekt uboczny". No i w sumie to jest to czego byśmy oczekiwali przy operacji na nietrwałej strukturze danych.
        List<Integer> even=new LinkedList<>();

        BiConsumer<List<Integer>,Integer> accumulator= (partial, elem)->{
            System.out.println("accumulator : "+Thread.currentThread().getName());
            partial.add(elem);
        };

        BiConsumer<List<Integer>,List<Integer>> combiner=(l1, l2)->{
            System.out.println("combiner : "+Thread.currentThread().getName());
            l1.addAll(l2);
        };
Kod wygląda podobnie do przykładu z reduce ale już nie trzeba na siłę zwracać listy po dodaniu elementu.

No to wiooooo :

List<Integer> result = Stream
                .iterate(0, e -> e + 1)
                .parallel()
                .filter(e -> e % 2 == 0)
                .limit(10)
                .collect(() -> even, accumulator,combiner); 

System.out.println(result);
i znowu jebłoooo
combiner : ForkJoinPool.commonPool-worker-3
accumulator : ForkJoinPool.commonPool-worker-6
accumulator : ForkJoinPool.commonPool-worker-5
combiner : ForkJoinPool.commonPool-worker-5
combiner : ForkJoinPool.commonPool-worker-5
combiner : ForkJoinPool.commonPool-worker-6
combiner : ForkJoinPool.commonPool-worker-2
combiner : ForkJoinPool.commonPool-worker-6
combiner : ForkJoinPool.commonPool-worker-2
combiner : ForkJoinPool.commonPool-worker-2
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    

Działająca wersja

No to w końcu coś co działa :

(...)
.collect(LinkedList::new, accumulator,combiner);

(...)
combiner : ForkJoinPool.commonPool-worker-4
combiner : ForkJoinPool.commonPool-worker-4
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Generalnie te porażki i potknięcia doskonale tłumaczą po co potrzebny jest Supplier. Każdy z niezależnych wątków musi mieć swój własny "nietrwały" półprodukt aby nie wchodzić sobie w drogę.

Collectors

Jak ktoś już Javą8 się bawił to wie, że jest 100 razy prostszy sposób na osiągnięcie tego wyniku :

.collect(Collectors.toList());

i należy go stosować i zasada DRY tutaj działa. Zapisywanie do listy to częsty i standardowy problem ze standardowym rozwiązaniem - w pracy go używaj. Tutaj słowo klucz to edukacja. Edukacja jest potrzebna aby usunąć symbolikę "abstrakcji kolektora". To trochę tak jak z Hibernate używanym przez ludzi nie znających baz danych. Czasem jak "działa ale nie wiadomo dlaczego" - to jesteśmy o krok od katastrofy. Bo jak nie wiadomo jak to działa to skąd wiadomo czy ten kawałek kodu znaleziony na necie rozwiąże akurat mój problem bez generowania pięciu innych?

Podobnie można podejść do tematu dokumentacji. Tak trzeba czytać. Ale jak sobie tak sam dla siebie eksperymentujesz - to jest to dużo lepszą formą nauki. Jeśli tylko nie pracujesz z zapalnikiem bomby atomowej - zrób sobie laboratorium. Postaraj się przewidzieć, że coś jebnie i faktycznie zweryfikuj. Na pewno w mózgu zostanie więcej niż gdy wszystko się od razu uda. Po to właśnie na Code Retreat usuwa się kod, żeby ludzie przypadkiem nie pomyśleli, że chodzi o kod a nie o czynność jego tworzenia. Jedyne co ma zostać po ćwiczeniu to nowe ścieżki pomiędzy neuronami.

środa, 1 czerwca 2016

Czytelność typów a procent prosty

"Kot rozszerza Animala" - tego typu przykłady są zazwyczaj tłem do nauki programowania. Czy to pomaga? Niedawno ktoś gdzieś podrzucił taki obrazek "Stefan mamy zlecenie na sklep ze zwierzętami - w końcu!! w końcu szkolenia na coś się przydadzą - Cat extends Animal ..."

Czasem trudno zrozumieć siłę pewnych mechanizmów programowania gdy przykłady użyte do ilustracji tychże mechanizmów są tak słabe jak polskie seriale komediowe. Ok ale co w zamian? Otóż można upiec dwie pieczenie na jednym ogniu i zarówno nauczyć się nie tylko jak ładnie programować rzeczy ale równocześnie a nawet bardziej jak ładnie programować przydatne rzeczy. A co jest bardzo przydatne? Bardzo przydatną wiedzą jest wiedza jak działa piniondz.

Nie tylko temat sam w sobie jest ciekawy ale podejście ludzi w jego kierunku jest ciekawe również : Tutaj jest Link do projektu reformy monetarnej w Islandii . O ile mi wiadomo Islandia została najbardziej wydymana na kryzysie i tam też ludzie są zdesperowani do znajdywania odpowiedzi na wiele ekonomicznych pytań - i ważne jest jak oni to robią. Dokument w linku ma 110 stron i to nie jest Lorem Ipsum, znajdują się tam wykresy, uzasadnienia, wykresy i uzasadnienia i tka dalej.

Dla odmiany co chwila na fejsbuku ktoś "udowadnia" (zazwyczaj swoje) tezy ekonomiczne demotywatorami, które zazwyczaj składają się z :
element 1) śmieszny obrazek
element 2) śmieszny komentarz do obrazka

I to w zasadzie tyle. (zazwyczaj każdy najpierw tworzy sobie tezę a później znajduje pasujące doń dowody to już inna kwestia...)

W każdym razie temat wydaje się dobry i rozwiązuje problem wymyślania ciekawych przykładów na warsztaty czy choćby do tego bloga. Na drodze pojawił się jednak pewien problem - otóż sam mam na ten temat niewiele pojęcia...

Rachunek odsetek prostych

Podświadomym zbiegiem okoliczności odkurzyłem tę oto książkę (młodsi czytelnicy mogą nie poznawać tego przedmiotu ale jest to popularna swego czasu tzw "papierowa książka") :

Która po nawet i ciekawym wstępie otwiera wzorem na liczenie odsetek prostych. I jest to już na tyle interesująca rzecz, że można przy jej okazji zbadać dosyć ciekawą technikę "ucztelniania kodu" dostępną w tzw. nowszych językach.

  • Ko - kapitał początkowy
  • r - stopa odsetek w skali jednostki czasu (dni, miesiące,lata)
  • T - liczba dni w jednostce czasu
  • t - ile dni kapitał pracował

Wzór jednocześnie jest prosty ale w kontekście jego implementacji może budzić masę kontrowersji i prowadzić do wielogodzinnych dyskusji o czytelności kodu (do wycieńczenia).

Implementacja

Zerknijmy na coś takiego :

def odsetki(kapital:Double,stopaOdsetek:Double,czas:Int,iloscDniWJednostce:Int)=
    kapital/100 * stopaOdsetek/iloscDniWJednostce * czas

Poza brakującym scaladokiem, który by tłumaczył czym dokładnie są argumenty reszta wygląda całkiem prosto i nawet można by to nazwać "prostym jednolinijkowcem". Również przez chwilę będziemy udawać, że operacje finansowe na Double to dobry pomysł aby ułatwić pokaz edukacyjny. Niebezpieczeństwo nadchodzi jednak z innej strony...

Pierwsza rzecz, która nas nie będzie interesować w tym artykule ale jest na tyle ciekawa, że warto o niej wspomnieć to ilość argumentów. Generalnie mało która zasada czytelności kodu ma pokrycie w kilkudziesięciu latach badań psychologicznych. Tutaj można sobie o tym więcej poczytać : Magiczna libcza 7 a aspekt praktyczny jest taki, że jak np. mamy 3 argumenty i każdy będzie wchodził w interakcję z innymi to mamy 2^3 kombinacji czyli "8" a według badań możemy maks 9 niezależnych elementów ogarnąć czyli 4 argumenty to już 2^4 - za dużo. To jest naprawdę ciekawe, że ta zasada ma jakiekolwiek potwierdzenie w badaniach a nie jest po prostu wygodna dla konsultanta żeby zwiększyć sumę na fakturze,

W każdym razie wywołanie :

odsetki(150.0,5.7,63,360)

Czy jesteś w stanie powiedzieć "co jest co?". Pierwszą próbą uczytelnienia kodu może być nazwanie argumentów (bo w scali można a w javie swego czasu pamiętam, że rozwiązaniem tego problemu było stworzenie buildera i konstrukcja w stylu new Domain.withParam1(...).andSomethingElse(...))

odsetki(
      kapital=150.0,
      stopaOdsetek = 5.7,
      czas=63,
      iloscDniWJednostce = 360)

Czyta się lepiej ale tylko czyta niestety. Generalnie poprawność tego wywołania zabezpiecza jedynie percepcja programisty, którą łatwo zachwiać nieprzyjemną sytuacją na drodze, kłótnią z teściową czy brakiem kawy w pracy (problemy pierwszego świata można tutaj mnożyć). Można do pomocy zaprząc kompilator zabezpieczając wywołanie odpowiednimi typami :

class Money(val value:BigDecimal) extends AnyVal
class InterestRate(val r:BigDecimal) extends AnyVal
class Days(val t:Int) extends AnyVal
class DaysInTimePeriod(val amount:Int) extends AnyVal

def interests(money:Money, rate:InterestRate, time:Days, daysinTimePeriod:DaysInTimePeriod)=
    money.value/100 * rate.r/daysinTimePeriod.amount * time.t

AnyVal i Domena

Klasa AnyVal jest tutaj bardzo istotna , ma ciekawą naturę gdyż w źródłach opisuje ją jakieś 100 razy więcej scaladoca aniżeli sama klasa ma kodu źródłowego. Generalnie jest to ukłon od kompilatora, który rozpakuje klasy rozszerzane przez AnyVal i w runtime zamieni jest w prymitywy - czyli w skrócie będzie "szybciej w runtime" ale "wolniej w kompilacji". Zawsze jest coś za coś. Jak ktoś myśli, że coś jest za darmo to jak młoda sarenka łyka wszystkie przekazy populistyczne. W każdym razie...

Wywołanie teraz wygląda jak poniżej :

interests(new Money(BigDecimal(150.0)),new InterestRate(BigDecimal(5.7)),
new Days(63),new DaysInTimePeriod(360))


Teraz jest bezpieczniej ale znowu mniej czytelnie i można by ponazywać parametry ale gdyby, gdyby gdyby możliwe było coś takiego :
interests("150.0" zloty,"5.7" ratePercent,63 days,360 daysPerYear)
Wygląda czytelniej (ocena subiektywna). (w ostatnim parametrze przekazujemy ilość dni w roku bo jest to wartość umowna - czyli tzw. część domeny. Książka twierdzi, ze bardzo często to wartość 360)

AnyVal i Opsy

Maszyneria jest do dyspozycji, wystarczy tylko zrobić odpowiednie "konwersje niejawne" :

 object InterestsImplicits{
    implicit class MoneyOps(val v:String) extends AnyVal{
      def zloty = new Money(BigDecimal(v))
    }

    implicit class InterestRateOps(val v:String) extends AnyVal {
      def ratePercent=new InterestRate(BigDecimal(v))
    }

    implicit class DaysOps(val v:Int) extends AnyVal{
      def days=new Days(v)
    }

    implicit class PeriodOps(val v:Int) extends AnyVal{
      def daysPerYear=new DaysInTimePeriod(360) //to mozna sobie gdzies zapamiętac by nie tworzyc za kazdym razem
    }
  }

I oto jeszcze raz ten kawałek zadziała :
 interests("150.0" zloty,"5.7" ratePercent,63 days,360 daysPerYear)

Jak już ktoś tykał implicitów to wie, że potrafią one budzić kontrowersje bo nie wiadomo co jest co w którym momencie. Tutaj raczej tego problemu nie ma bo wykorzystujemy ten mechanizm tylko i wyłącznie by uzyskać czytelniejsza formę "metod fabrykujących" i zamiast zloty("200.0") mamy "200.0" zloty co czyta się po ludzku i jest moim niezmiernie skromnym zdaniem również poprawną ilustracją użycia tego ficzeru, że nie trzeba kropki pisać.

No dobra a co z tymi dodatkowymi klasami? Ano nic bo ich tak naprawdę nie będzie w runtime przez to, że dziedzicza po anyval (i znowu kosztem jest czas kompilacji). Ba nawet to zastosowanie jest wymienione w oficjalnej dokumentacji http://docs.scala-lang.org/overviews/core/value-classes.html w sekcji Extension methods

Kod wydaje się i czytelny i zabezpieczony a ten ostatni temat "metody rozszerzone" jest dobrą okazją aby zbadać temat od trochę innej strony.

Extension Methods

Poniższy Kod to już język Kotlin a kod wydaje się jakby mniej skomplikowany i prostszy do ogarnięcia (ale zaraz będzie jedno ważne ale):

class Money(val value: BigDecimal)
class InterestRate(val r: BigDecimal)
class Days(val t:Int)
class DaysInTimePeriod(val amount:Int)

//extensions!
fun String.zloty(): Money = Money(BigDecimal(this))
fun String.ratePercent()= InterestRate(BigDecimal(this))
fun Int.days()= Days(this)
fun Int.daysPerYear()= DaysInTimePeriod(360)

interests2("150.0".zloty(), "5.7".ratePercent(), 63.days(), 360.daysPerYear())

Przede wszystkim ten przykład ma pokazać, że to podejście nie jest tylko "scalowe" . Druga kwestia to zrozumienie jak pewne konstrukcje językowe określają jego charakter. Extension Methods (lepiej zostać przy angielskim żeby było wiadomo co jest co) są dużo prostsze do ogarnięcia w Kotlinie aniżeli w Scali gdyż w tej drugiej wymagają znajomości potężniejszego mechanizmu Implicits. AAAALLLLEEE z drugiej strony jak już znamy Implicits możemy zaimplementować inne mechanizmy jak choćby TypeClasses a Extension Method służy... no chyba tylko do Extension Methods. Tak czy inaczej Java nie ma ani jednego ani drugiego a "project lombok" jest uważany w pewnych kręgach za szczyt ku*wa technologii kosmicznej 22 wieku

"Bo kod się częściej czyta..."

Czasem przy okazji emtikonek w sali w stylu "(_+_)" zagorzali wielbiciele nawiasów i średników lubią podkreślać, że kod się częściej czyta niż pisze. I jest to jak najbardziej prawda ale nie jest to żadnym argumentem na rzecz barokowego języka. Chociaż czytelność to rzecz subiektywna i jest wskazane aby team (pol. drużyna) sama sobie ustaliła warianty jakości kodu to jednak ten artykuł mam nadzieję pokazał, że bogatsze języki umożliwiają pisanie czytelniejszego i jednocześnie bezpieczniejszego kodu aniżeli klasyczna Java.