Na tej pętli nie zawiśniesz

pawelwlodarski.blogspot.com 8 lat temu

To jest taki trochę ciąg dalszy poprzedniego odcinka Kompozycja Funkcyjna w języku w którym lista dziedziczy po funkcji, który to odcinek był inspirowany dokumentem Why Functional Programming Matters.

O ile poprzednia część była o prostej kompozycji i funkcjach wyższego rzędu o tyle dalsza część wchodzi w zupełnie nowy wymiar komponowania programów dodając kontrolę nad wymiarem czasu. No bo to jest trochę taki mindfuck kiedy do tego "co jest" jako konstrukcja kodu dodajemy wymiar "kiedy i czym to jest" - wymiar który również można parametryzować. Ale najpierw ogólnie o kompozycja na rysunku.

Kompozycja jest tak naprawdę udana kiedy bierzemy dwie części i łączymy je bez dodatkowego "klejo-kodu"

A tak mniej abstrakcyjnie. Na poniższym rysunku nakrętka doskonale komponuje się z butelką i tworzy nową funkcjonalność "zamknięta butelka".

Następnie spróbujmy uzyskać zamkniętą butelkę poprzez kompozycje butelki i mapy Jury Krakowsko-Częstochowskiej (Część północna).

Pomimo iż, dobry konsultant może się wykłócić, iż tak otrzymany produkt spełnia wymagania user story "as a Stefan I want to zabrać ze sobą za sklep 1 litr bimbru so that straż miejska mnie nie spisze " i wszelkie "rozlania płynu" wynikają z błędnego użycia interfejsu przez użytkownika - to jednak my - inżynierowie - wiemy w głębi duszy, iż kompozycja jest nieudana

Bardzo często w takich przypadkach wywala się masę hajsu na jakiś zewnętrzny frejmłork obiecujący mimo wszystko przymocowanie mapy do butelki w sposób trwały za 100 trylionów rubli per sprint

(Odkręcanie zrobi się później w ramach utrzymania - a w przyszłości bez kleju już się nie obędzie także "vendor lock in")

Czas

No i teraz ten mindfuck. Generalnie kawałki kodu widzimy tu i teraz i myśląc o kompozycji myślimy o "kompozycji tu i teraz" w "formie zdefiniowanej tu i teraz". Idąc jeden poziom abstrakcji wyżej można wyobrazić sobie kompozycję w kontekście czasu - "skomponuj jeden fragment kodu, który kiedyś będzie działał i dopiero wtedy będzie wiadomo jak wygląda - z drugim kawałkiem kodu, który też w pewnym momencie czasu będzie pasował do kompozycji".

Mi zbudowanie intuicji wokół tej koncepcji zajęło dużo czasu i zauważyłem, iż atakowanie tego tematu z różnych kierunków buduje interesujące perspektywy pojęciowe, które pomagają zbudować swoiste "przyczółki zrozumienia". Także od ostrej abstrakcji przejdźmy teraz do zwykłych konkretów.

Pętla

Jest sobie taka zwykła pętla.

var i = 0
while(i < 100){
    println(i)
    i=i+1
}

Co można zrobić z taką zwykłą pętlą? Rozwiązuje ona doskonale pewien niszowy problem pod tytułem "wypisz 100 liczb na konsolę" ale średnio komponuje się z czymkolwiek. Na początek może spróbujmy dokodować możliwość kompozycji ze zwykłą liczbą całkowitą.

def
kompozycja(limit:Int)={
   var i = 0
while(i < limit){
      println(i)
      i=i+1
    }
  }

kompozycja(10)

Możemy iść dalej i dorobić gwint do wkręcania czynności jaka ma być wykonana wewnątrz pętli. (Kto był w technikum ten wie jak fajnie się gwintuje gwintownikiem ręcznym przez 5 godzin). I teraz można to zrobić o tak :

trait BusinessGwintownikStrategy{
  def
dziaaaaaaaaałaj(input:Int):Unit
}


object WypisywaczProfessionalStrategy extends BusinessGwintownikStrategy{
  override
def
dziaaaaaaaaałaj(input: Int): Unit = println(s"STRATEGY $input")
}

def
kompozycja(limit:Int,strategy : BusinessGwintownikStrategy)={
  var i = 0
while(i < limit){
    strategy.dziaaaaaaaaałaj(i)
    i=i+1
  }
}

kompozycja(10,WypisywaczProfessionalStrategy)

Tylko na ch*j? W tej całej strategi nie ma choćby jakiegokolwiek stanu także po co się męczyć z jakimś pseudo obiektowym rękodziełem kiedy mamy uniwersalny standard śrubek w formie A=>B? (Chociaż UMLe zawsze można za dodatkowy hajs ojebać. No i do tego fajnie wyglądają w powerpointach)

I to jest dosyć istotny moment, iż choćby go wyboldowałem. Bo teraz tak. Jak macie np. kompa to on ma interfejs USB. I są pendraivy na USB, klawiatury na USB, lampki na USB i podobno choćby takie "urządzenia peryferyjne" dla zdalnych par (if you know what i mean). Mamy do czynienia z ogólnoświatowym standardem dzięki czemu lampka na usb wyprodukowana w Chinach komponuje się z komputerem wyprodukowanym na tajwanie.

Gdyby każdy wyprodukowany komp dorzucał swój interfejs "Strategia" to nie byłaby to "Strategia" tylko "Tragedia". Dlatego warto użyć universalnego portu w postaci funkcji , która jest zdefiniowana w bibliotece standardowej i jest U-N-I-V-E-R-S-A-L-N-A (później jak ktoś bardzo chce może sobie ograniczyć dostęp ze względu na typy - dajemy wybór)

val wypisywacz : Int =>
Unit = i => println(s"FUNKCYJA $i" )


def
kompozycja(limit:Int,strategy : Int =>
Unit)={
  var i = 0
while(i < limit){
    strategy(i)
    i=i+1
  }
}

kompozycja(10,wypisywacz)

Gdzie ten czas?

No dobra to nadszedł czas na "czas". W tej chwili mamy dwa sposoby kompozycji (INT, INT => UNIT) z tymże w takiej formie musimy materializować je w czasie w tym samym momencie (tak wiem, iż w scali można robić częściową aplikację parametrów w zwykłych metodach bo w scali można "wszystko" ale dla wygody wyprowadzanego wywodu nie mąćmy).

Aby odseparować te dwie rzeczy w czasie wykorzystajmy mechanizm wspomniany w pierwszej części "Why Functional Programming Matters" czyli Currying.

def
kompozycja(limit:Int)(strategy : Int =>
Unit):Unit={
  var i = 0
while(i < limit){
    strategy(i)
    i=i+1
  }
}

val kompozycjaWToku: (Int =>
Unit) =>
Unit = kompozycja(10) _
println("cos sobie wypisze dla zabicia czasu")
kompozycjaWToku(wypisywacz)

Ba! możemy pójść choćby o krok dalej i nie wykonywać programu w tym samym czasie co ustalenie ostatniej kompozycji.

//Metoda zwraca teraz typ () => Unit - nowy alias 'Program'
type
Program = () =>
Unit
def
kompozycja(limit:Int)(strategy : Int =>
Unit):Program=()=>{
  var i = 0
while(i < limit){
    strategy(i)
    i=i+1
  }
}

val kompozycjaWToku: (Int =>
Unit) => Program = kompozycja(10) _
println("kompozycja w trakcie...")
val blueprint: Program =kompozycjaWToku(wypisywacz)
println("Jakiś czas później...")
blueprint()

Iteracja

Analogicznie do funkcji, która określa "co robić" możemy dodać dodatkową kompozycję "jak iterować."

val coDrugi: Int =>
Int = _ + 2
type
Program = () =>
Unit
def
kompozycja(iterator:Int=>Int)(strategy : Int =>
Unit)(limit:Int):Program=()=>{
  var i = 0
while(i < limit){
    strategy(i)
    i=iterator(i)
  }
}

val iterujCoDrugi: (Int =>
Unit) => (Int) => Program = kompozycja(coDrugi) _
val wypisuj: Int => Program =iterujCoDrugi(wypisywacz)
val doDziesieciu : Program= wypisuj(10)
println("Jakiś czas później...")
doDziesieciu()

Nie ma róży bez kolców. To co źle tutaj działa to połączenie sposobu generowania elementów z licznikiem pętli. Na pytanie "po co byśmy chcieli to robić" odpowiemy kolejnym przykładem z artykułu Why Functional Programming matters a mianowicie iteracyjnym algorytmem na obliczenia pierwiastka kwadratowego

Czyli generalnie chciałbym aby pętla sobie chodziła - choćby i w nieskończoność - a ja niezależnie od niej co iterację chcę generować kolejne stadium rozwiązania.

Pętla tylko koncepcyjna

Można wytwarzać kolejne przybliżenia rozwiązania naszą pętlą ale zaczyna powstawać coś strasznego - metoda z tysiącem parametrów. A do tego utrudnione jest użycie zwykłych funkcji bo one w scali nie mogą mieć generyków

val wypisywacz : Int =>
Unit = i => println(s"FUNKCYJA $i")
val coDrugi: Int =>
Int = _ + 2
val nextRoot : Double=>Double=>Double = n => ai => (ai+n/ai)/2
def
generycznyWypisywacz[A] :A=>Unit =e => println(s"e : $e")

type
Program = () =>
Unit
def
kompozycja[A](start:A)(iterator:A=>A)(strategy : A =>
Unit)(limit:Int):Program=()=>{
  var i = 0
var e=start
  while(i < limit){
    strategy(e)
    e=iterator(e)
    i=i+1
  }
}

//kompozycja(0.0)(nextRoot(2))(wypisywacz)(10)  // zwykly wypisywacz nie dziala
val program=kompozycja(1.0)(nextRoot(2))(generycznyWypisywacz)(10)

program()

Ale co gorsza po odpaleniu programu nie mamy żadnego wpływu na wynik naszej operacji. Zwróć uwagę na kilka ostatnich elementów. :

e : 1.0
e : 1.5
e : 1.4166666666666665
e : 1.4142156862745097
e : 1.4142135623746899
e : 1.414213562373095
e : 1.414213562373095
e : 1.414213562373095
e : 1.414213562373095
e : 1.414213562373095

I teraz fajnie, iż robimy sobie takie wstrzykiwanie zależności i parametrów ale my wcale nie potrzebowaliśmy 10 elementów bo z tego co widać 5 by wystarczyło. Ale to jest informacja, która będzie dostępna dopiero w trakcie wykonania. Czyli chociaż nasze budowanie programu jest takie trochę lazy to nijak nie umiemy tego wykorzystać w trakcie odpalania programu.

Oczywiście moglibyśmy dodać kolejny parametr w postaci funkcji A=>Boolean, który by określał warunek w pętli while ale specjalnie już trochę przekombinowałem. Nosz kurde mamy 4 parametry w metodzie - zaraz się z tego zrobi WinAPI.

Aby rozwiązać ten problem musimy totalnie zanihilować fizyczną reprezentację pętli i zachować ją jedynie w formie "koncepcji", która będzie materializowana w miarę potrzeb w trakcie wykonywania się programu.

Leniwość i Materializacja

Tutaj trochę robi się obiektowo bo tworzymy klase - ale to case klasa czyli taka klasa-dana ale nie jak w piosence oj dana dana tylko dana jako dana... w każdym razie "koncepcyjna pętla, która można iterować bez końca" wygląda tak

case
class Loop[A](e:A,next:()=>Loop[A])

def
petlaKoncepcyjna[A](start:A)(iterator:A=>A): Loop[A] = {
    val e2=iterator(start)
    Loop(e2,()=>petlaKoncepcyjna(e2)(iterator))
}

val Loop(e,next)=petlaKoncepcyjna(1.0)(nextRoot(2))
val Loop(e2,next2) = next()
val Loop(e3,next3) = next2()
val Loop(e4,next4) = next3()

println(s"elements : $e,$e2,$e3,$e4")
I wynik :
elements : 1.5,1.4166666666666665,1.4142156862745097,1.4142135623746899

Nie ma co się na razie przerażać tym, iż jedziemy po niej manualnie. Co się robi na pałę da się zautomatyzować (dlatego większość programistów CRUDa kiedyś straci prace - just saying)

Co można zrobić dalej? Udajmy się po inspirację do języka czysto funkcyjnego...

Bez Klas

Haskellowo - amatorska implementacja tego co do tej pory zrobiliśmy wygląda mniej więcej tak :

data Loop a = Loop a (Loop a)

petlaKoncepcyjna
::
a
-> (a->a) ->
Loop
a
petlaKoncepcyjna start iter = Loop e (petlaKoncepcyjna e iter)
  where e = iter start

root
::
Double
->
Double
->Double
root n ai = (ai+n/ai)/2

element (Loop a _) = a
loop (Loop _ next) = next

Co daje nam ponownie możliwość manualnej iteracji po pętli.

*Learn> let s1=petlaKoncepcyjna 1.0 (root 2)
*Learn> element s1
1.5
*Learn> let s2= loop s1
*Learn> element s2
1.4166666666666665
*Learn> let s3= loop s2
*Learn> let s4= loop s3
*Learn> let s5= loop s4
*Learn> map element [s1,s2,s3,s4,s5]
[1.5,1.4166666666666665,1.4142156862745097,1.4142135623746899,1.414213562373095]

Tyle, iż nie ma co wyważać otwartych drzwi bo w Haskellu mamy już na to gotowca

> :t iterate
iterate :: (a -> a) -> a -> [a]

To będzie szło w nieskończoność dlatego mamy specjalny operator do określenia ile elementów nam potrzeba - w ten sposób uniezależniliśmy się od wewnętrznej iteracji po pętli

> take 10 $ iterate (root 2) 1.0
[1.0,1.5,1.4166666666666665,1.4142156862745097,1.4142135623746899,
1.414213562373095,1.414213562373095,1.414213562373095,1.414213562373095,1.414213562373095]
*Learn> :t take
take :: Int -> [a] -> [a]

Co więcej uniezależniliśmy się od sposobu określania "końca pętli"

> :t takeWhile 
takeWhile :: (a -> Bool) -> [a] -> [a]
*Learn> takeWhile (<10) $ iterate (+1) 1
[1,2,3,4,5,6,7,8,9]

Ok do szczęścia brakuje nam już tylko jednej rzeczy. Na razie operujemy na pojedynczym elemencie a na przykład chcielibyśmy określić by pętla się skończyła gdy elementy przestaną się zmieniać - a do tego musimy jakoś je ze sobą porównywać.

> let epsilon e (a,b) = abs (b-a) < e 
*Learn> epsilon 0.1 (2,1)
False
*Learn> epsilon 0.1 (2,2.01)
True

Czas na kolejny mindfuck - ponieważ nasza pętla jets teraz koncepcją wiec nie ma problemu by ja skomponować ...samą ze sobą. I do tego też jest sprzęt.

> :t zip
zip :: [a] -> [b] -> [(a, b)]

W zasadzie wszystkie części gotowe - to wio!

Learn> let squareRootSeq=iterate (root 2) 1.0
*Learn> head $ dropWhile (not . (epsilon 0.01)) $ zip squareRootSeq (tail  squareRootSeq)
(1.4166666666666665,1.4142156862745097)
*Learn> snd $ head $ dropWhile (not . (epsilon 0.01)) $ zip squareRootSeq (tail  squareRootSeq)
1.4142156862745097

Pierwsza uwaga dla hejterów, którzy będą płakać iż tam się za dużo dzieje - ludzie... to jest REPL, w programie możecie sobie przy pomocy "where" albo "let .. in" porobić aliasy/zmienne i będzie czytelnie.

No a druga sprawa - rozwiązaliśmy problem iteracyjny bez użycia kleju komponując jedynie ze sobą poszczególne komponenty. Według autora wspominanego od czasu do czasu dokumenty "Why Functional Programming matters" taką klasyczną pętelką w jakichś kobolach wyglądało by tak :

W Javie po imperatywnemu na podstawie znalezionego w necie przykładu mamy cos takiego :

public
class
SqrtNewtonMethod {
    public
static
void
main(String[] args) {
        double c =
Double.parseDouble(args[0]);

        double epsilon =
1e-15;    // relative error tolerance
double t = c;              // estimate of the square root of c
// repeatedly apply Newton update step until desired precision is achieved
while (Math.abs(t - c/t) > epsilon*t) {
            t = (c/t + t) /
2.0;
        }
        // print out the estimate of the square root of c
System.out.println(t);
    }
}

Tyle, iż te kawałki kodu to znowu monolityczna pętla tak jak ta z początku co rozwiązywała jeden niszowy problem!!!

Inny Problem

Aby pokazać siłę mechanizmu, którym dysponujemy pykniemy inny interesujący problem - ciąg Fibonacciego. Każdy student wie, iż są dwa rozwiązania : czytelne rekurencyjne, które wypierdala stos i drugie - nieczytelne iteracyjne z tymczasowym akumulatorem.

A tu proszę! Pojawia się trzecie, czytelne i bezpieczne dla stosu. Najpierw określamy minimum logiki specyficznej dla naszego problemu.

let fibStep (a1,a2) = (a2,a1+a2)

I KOMPOZYCJA!

> map fst $ take 20 $ iterate fibStep (0,1)
[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181]

Oczywiście to jest czytelne jak się język choć trochę zna. No bo bez sensu mówić, iż jest "nieczytelne" jak się języka nie zna. To tak jakby Stefan pod budką powiedział, iż język Duński jest niezrozumiały bo Stefan go nie zna. Bo według Stefana to jest cecha języka Duńskiego, ze Stefan go nie rozumie a nie cecha samego Stefana.

W każdym razie może to dobry czas na rzucenie flary i przekierowanie nienawiści w inna stronę poprzez zacytowanie jak Diekstra krytykuje w 2001 "Budget Council of The University of Texas." za to, iż zamienili na studiach Haskella na Jave o pod tym linkiem

I z klasami

Generalnie cały wywód z pętla w Scali wyprowadzał taką ułomna implementację Strumienia, który już jest w bibliotece standardowej zoptymalizowany i gotowy do użycia.

//iterate podobnie jak w HAskellu
    Stream.iterate(1.0)(nextRoot(2)).take(5).toList
    //List(1.0, 1.5, 1.4166666666666665, 1.4142156862745097, 1.4142135623746899)
val rootTwo: Stream[Double] =Stream.iterate(1.0)(nextRoot(2))

    //zip dwóch streamów
    rootTwo.zip(rootTwo.tail).take(5)


    //fibonacci
val fibStep: ((Int,Int)) => (Int,Int) = { case (a1,a2) => (a2,a1+a2) }

    Stream.iterate((0,1))(fibStep).take(20).map(_._2).toList
    //List(1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765)

choćby w Javie

Otóż tak - można to zrobić choćby w Javie! (z domieszką javaslang)

private List<Integer> fibonacci(int limit){
        Tuple2<Integer, Integer> seed = Tuple.of(0, 1);

        UnaryOperator<Tuple2<Integer,Integer>> step=
                t->Tuple.of(t._2,t._1+t._2);

        return Stream.iterate(seed, step)
                .limit(limit)
                .map(t -> t._1)
                .collect(Collectors.toList());
 }

Także to już nie jest kwestia, który język lepszy-gorszy ale raczej krytyka pewnego sposobu myślenia. I to w sumie ciekawie nawiązuje do wstępu. Otóż okazuje się, iż kompozycja w Javie również ma charakter czaso-przestrzenny. Do 2014 są kompozycyjne wieki ciemne a później odrodzenie...

Praktyka

Teoria teorią ale bez ćwiczeń mięśnie nie rosną. Dlatego spróbojemy warsztat na podstawie artykułu "Why Functional Programming Matters" : "Functional Programming Matters" w Scali,Haskellu i Javie8 .

Plan jest taki by robić w Scali i Javie8 ćwiczenia inspirowane artykułem a później dla porównania i perspektywy edukacyjnej zobaczyć jak to będzie wyglądało w Haskellu. 13 października w czwartek o 17:30. Także zapraszam.

Mobilizacja

Konferencja Mobilization już 22 października. Bilety można kupować tutaj --> o tutaj



Idź do oryginalnego materiału