niedziela, 26 lipca 2015

Wstrzykiwanie po fakcie - czyli funkcje w życiu codziennym

Kiedyś na bazarze kupiłem książkę do karate za 12 złotych (tak - jest coś takiego jak książka do karate, są tam obrazki i nawet sekcja jak walczyć przy pomocy parasola lub jak obezwładnić przeciwnika kantem stołu). Poza kolejnymi ilustracjami przedstawiającymi fazy kopa z półobrotu jest tam także wstęp filozoficzny o nauce.

AbstraHUJąc od masy japońskich słówek generalnie chodzi o to, aby po osiągnięciu pewnego poziomu umiejętności cofnąć się niejako do poprzedniego etapu tak by uprzednio zdobyta wiedza nie zaowocowała uprzedzeniami przy zdobywaniu nowej.

Co się stanie gdy pozostaniemy na abstrakcyjnym poziomie specjalisty? Jeśli np. opanowaliśmy kop z obrotu i przejdziemy do nauki ciosu podbródkowego (coś jak mortal kombat) to dopiero po odpowiedniej ilości ćwiczeń możemy posługiwać się techniką w odpowiedni sposób. Gdy przysiądziemy do tego z nastawieniem "jestem specem" to przy pierwszych niepowodzeniach może pojawić się racjonalizacja "coś mi to nie wychodzi, ale przecież jestem specem, jestem zwycienzcom - AHA! - czyli to jest głupie, to jest głupie - wracam do Javy... tfu znaczy do kopów z półobrotu"

jak działa IF?

Gdybyśmy mieli komuś kto nie wie co to jest if wytłumaczyć czym on jest i po co on jest - jakbyśmy do tego podeszli?

  1. Pierwszy sposób to najpierw kilka prostych ćwiczeń oderwanych od rzeczywistości aby zrozumieć mechanikę a później jakiś przykład z życia (np if(isLogged(user))) by zrozumieć sens
  2. Podejście drugie : dwugodzinna dyskusja o wyższości jednego rozwiązania nad drugim po czym wszyscy idą do domu i się nie odzywają do siebie
I pomimo iż sposób drugi jest bardzo często praktykowany w społecznościach IT - szczególnie w kontekście nauki FP - to my spróbujemy podejścia numer 1

Cel ćwiczenia

Ćwiczenie jest wzięte z książki : "Functional and Reactive Domain Modeling"


Mamy taką oto implementację Serwisu
trait AccountService[Account, Amount, Balance] {
def open(no: String, name: String, openingDate: Option[Date]):
AccountRepository => Try[Account]
#A
def close(no: String, closeDate: Option[Date])
AccountRepository => Try[Account]
def debit(no: String, amount: Amount): AccountRepository => Try[Account]
def credit(no: String, amount: Amount): AccountRepository => Try[Account]
def balance(no: String): AccountRepository => Try[Balance]
}

Kodu może wydawać się trochę pokręcony - co by nie było jest on wyciągnięty z końcówki rozdziału - toteż skoncentrujmy się na razie jedynie na typie zwracanym: AccountRepository => Try[Account].

Ten typ to zwykła funkcja i przez taką formę wyniku możemy wykonać operacje na repozytorium... bez podawania repozytorium. Dokładnie. Jest to forma dependency injection, której jeszcze nie widziałem - czyli wstrzykiwanie już niejako po operacji.

object App {
import AccountService._
def op(no: String) = for {
_ <- credit(no, BigDecimal(100))
_ <- credit(no, BigDecimal(300))
_ <- debit(no, BigDecimal(160))
b <- balance(no)
} yield b
}

I następnie :
scala> import App._
import App._
scala> val domainAction= op("a-123")
domainAction: AccountRepository => scala.util.Try[Balance] = <function1>

scala> domainAction(domainRepository)
...

I to działa*...tylko ku*wa jak?! (* - działa jak w dowolny sposób doda się map i flatMap do Function1 ale o tym za chwilę)

Aby zrozumieć co tam się dzieje robimy labolatorium (jak za starych czasów na TVP1)

Jak działa map?

Pierwsza rzecz to nie ma co rysowac od nowa tych obrazków bo jest ich masa w sieci
1. Tutaj jest pierwszy
2. A tutaj następny

Od strony mechaniki to będzie wyglądało tak :

val f=(i:Int)=>i+1
List(1,2,3) map f
Some(1) map f

f: Int => Int = 
res8: List[Int] = List(2, 3, 4)
res9: Option[Int] = Some(2)

I możemy nawet dla porównania zrobić w Javie jak ktoś lubi
Function<Integer,Integer> f = (Integer i) -> i + 1;
    Stream.of(1,2,3).map(f).forEach(out::println);
    Optional.of(1).map(f).ifPresent(out::println);

A jak działa map na funkcji?

Z tego co widziałem i doświadczyłem ludziom na etapie "Java 1-7" trochę grzeje banie fakt traktowania funkcji jako argumentu innej funkcji czy też rezultatu zwracanego jako funkcji. W związku z tym analiza tego przykładu może zając chwilę.

val f=(i:Int)=>i+1
((i:Int)=>i+2) map f
//lub
val f2=(i:Int)=>i+1
val fmapped=f2 map f
fmapped(1)

f: Int => Int = 
res8: Int => Int = 
//lub
f2: Int => Int = 
fmapped: Int => Int = 
res9: Int = 4

Aby to zadziałało trzeba albo samemu dodać map do Function1 albo zaimportować jakaś bibliotekę, która to robi za nas. Z rezultatu (3) widzimy, że niejako dwie funkcje (+1) i (+2) zostały złożone i jeśli spojrzymy w implementację to faktycznie tak to wygląda
implicit def Function1Functor[R]: Functor[...cuda...] {
    def fmap[A, B](r: R => A, f: A => B) = r andThen f
  }

jak działa flatMap

List(1,2,3).map(i=>List(i,i))
List(1,2,3).flatMap(i=>List(i+1))
Some(1).map(i=>Some(i+1))
Some(1).flatMap(i=>Some(i+1))

i wynik
res8: List[List[Int]] = List(List(1, 1), List(2, 2), List(3, 3))
res9: List[Int] = List(2, 3, 4)
res10: Option[Some[Int]] = Some(Some(2))
res11: Option[Int] = Some(2)

A w Javie
Stream.of(1,2,3).map(i->Stream.of(i)).forEach(out::println);
Optional.of(1).map(i->Optional.of(i)).ifPresent(out::println);
Stream.of(1,2,3).flatMap(i->Stream.of(i)).forEach(out::println);
Optional.of(1).flatMap(i->Optional.of(i)).ifPresent(out::println);

Po co umieszczać jeszcze przykłady w Javie? Głównie dla porównania (ale tez aby oduczyć ludzi, ze porównywanie Java vs Scala to porównanie Obiektowo vs Funkcyjnie)

Mindfuk start - flat map dla funkcji

To jest coś co dla mnie nie było na początku intuicyjne ale po zerknięciu w implementację

implicit def Function1Bind[R]:  = new Bind[...cuuuuda....] {
    def bind[A, B](r: R => A, f: A => R => B) = (t: R) => f(r(t))(t)
}

Pierwsza rzecz dlaczego nazywa się to bind? Bo w haskellu nazywa się to bind a scali i javie flatMap a w scalaz wzorowali się chyba na haskellu(deal with it).

No i generalnie flatMap ma ogólnie sygnaturę Cos.flatMap(arg=>Cos) i też tutaj jest podobnie F.flatmap(arg=>F)

// implementacja (t: R) => f(r(t))(t)
// dla 1 f(r(1))(1) => f(2)(1) => 2+1 => 3
// dla 2 f(r(2))(2) => f(3)(2) => 3+2 => 3
val f2=(i:Int)=>i+1
val fflatmapped=f2 flatMap(a=>b=>a+b)
fflatmapped(1)
fflatmapped(2)

O co chodzi z tym podkreślnikiem?

// implementacja (t: R) => f(r(t))(t)
// dla 1 f(r(1))(1) => f(_)(1) => 1 => 1
// dla 2 f(r(2))(2) => f(_)(2) => 2 => 2
val f2=(i:Int)=>i+1
val fflatmapped=f2 flatMap(_=>b=>b)
fflatmapped(1)
fflatmapped(2)

W tym przypadku centralnie zlewamy wynik wywołania pierwszej funkcji- czy to ma sens? Tylko w jednym przypadku...
// implementacja (t: R) => f(r(t))(t)
// dla 1 f(r(1))(1) => f(_)(1) => 1 => 1
// dla 2 f(r(2))(2) => f(_)(2) => 2 => 2
val f2=(i:Int)=>{
  println("side effect!!")
  i+1
}
val fflatmapped=f2 flatMap(_=>b=>b)
fflatmapped(1)
fflatmapped(2)

Czas na połączenie elementów ukladanki...

map, flatMap, funkcje i repozytorium

class Repo{
  def save(i:Int,actionId:String):Option[Int]={
    println(s"saving $i in $actionId")
    Some(i)
  }
}
def action1(domainArgument:Int):Repo=>Option[Int]={repo=>
  repo.save(domainArgument,"akcja1")
}

def action2(domainArgument:Int):Repo=>Option[Int]={repo=>
  repo.save(domainArgument,"akcja1")
}

val domainAction=action1(1).flatMap(_=>action2(1))
domainAction(new Repo())

Nie wiem czy ten kawałek kodu będzie jasny od początku. Generalnie udało nam się zbindować (AHA! dlatego to nazywa się bind - oczywiście źle, ze używam słów zapożyczonych z angielskiego) kilka operacji które będą wykonane na repo bez specyfikowania skąd wziąć to repo. Czy to jakas forma https://en.wikipedia.org/wiki/Late_binding - być może...

I podobny rezultat :

def wio(entry:Int)=for{
  _ <- action1(entry)
  result <- action2(entry)
} yield result

val domainAction=wio(1)
domainAction(new Repo())

saving 1 in akcja1
saving 1 in akcja1
res12: Option[Int] = Some(1)

Jedno małe ale...

Małe ale polega na tym, że w tzw. "for-comprehension" ostatnim elementem musi być map a u nas jest flatMap. Ponieważ rozwiązanie oczywiste mi umykało z przed oczu i metoda prób i błędów nie mogłem znaleźć rozwiązania co to map maiłoby robić toteż zbudowałem Zestaw młodego dekompilatora

W SBT

initialCommands in console := "import scalaz._, Scalaz._"

:paste i wklei https://gist.github.com/Luegg/7449370
scala> desugar{
     | for{
     |   _ <- action1(1)
     |   result <- action2(1)
     | } yield result
     | }


I wynik
scalaz.Scalaz.ToBindOpsUnapply[Repo => Option[Int]]($line18.$read.$iw.$iw.$iw.$iw.$iw.$iw.$iw.$iw.$iw.$iw.action1(1))
(scalaz.this.Unapply.unapplyMAB2[scalaz.Bind, Function1, Repo, Option[Int]]
(scalaz.Scalaz.function1Covariant[Repo])).flatMap[Option[Int]]
(((_: Option[Int]) => scalaz.Scalaz.ToFunctorOpsUnapply[Repo => Option[Int]]
($line18.$read.$iw.$iw.$iw.$iw.$iw.$iw.$iw.$iw.$iw.$iw.action2(1))
(scalaz.this.Unapply.unapplyMAB2[scalaz.Functor, Function1, Repo, Option[Int]]
(scalaz.Scalaz.function1Covariant[Repo])).map[Option[Int]](((result: Option[Int]) => result))))

Nie ma co się przerażać tym kodem gdyż interesuje nas tylko końcówka : .map[Option[Int]](((result: Option[Int]) => result))
I to jest to oczywiste rozwiązanie :
val domainAction=action1(1).flatMap(_=>action2(1).map(i=>i))(new Repo())
val domainAction2=action1(1).flatMap(_=>action2(1).map(identity))(new Repo())

Chyba

Brak komentarzy:

Prześlij komentarz