Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Użyteczne programowanie funkcyjne

Użyteczne programowanie funkcyjne

Przegląd narzędzi programisty funkcyjnego pod kątem użyteczności

Avatar for Piotr Krzemiński

Piotr Krzemiński

January 31, 2019
Tweet

More Decks by Piotr Krzemiński

Other Decks in Programming

Transcript

  1. Plan na dziś funkcje jako wartości przezroczystość referencyjna efekty uboczne

    rekursja funkcje wyższych rzędów niemutowalność klasy typów czysto funkcyjne wejście/wyjście
  2. O mnie 2+ lat doświadczenia w Elmie teoria jęz. programowania,

    systemy typów, algorytmy optymalizacyjne projekty open source: 5+ lat doświadczenia w Scali chimney - https://github.com/scalalandio/chimney endpoints - https://github.com/julienrf/endpoints octopus - https://github.com/krzemin/octopus @pkrzemin
  3. 1. Przekazanie funkcji jako argument do innej funkcji 2. Zwracanie

    funkcji jako rezultat wywołania funkcji 3. Przypisanie funkcji do zmiennej def f1(g: Int => Int): Int = g(2) f1(x => 3 * x) def f2(x: Int): Int => Int = { y: Int => y + x } f2(5)(4) val f3 = f2(10) f3(20)
  4. Gdzie to może być przydatne? 1. Kompozycja funkcji def compose[A,

    B, C](f: A => B)(g: B => C): A => C = { a => g(f(a)) } val squareMinus1 = compose(x => x * x)(_ - 1) squareMinus1(5) // 24
  5. Gdzie to może być przydatne? 3. Funkcje wyższych rzędów val

    xs = List(1, 2, 3, 4, 5) xs.map(plus(2)) // List(3, 4, 5, 6, 7)
  6. Wyrażenie jest przezroczyste referencyjnie jeżeli może być zastąpione swoją wartością

    bez zmiany zachowania programu. Math.sqrt(x * x - d1) - Math.sqrt(x * x - d2) val sqx = x * x Math.sqrt(sqx - d1) - Math.sqrt(sqx - d2)
  7. Przykład 1. Math.sqrt(x * x - d1) - Math.sqrt(x *

    x - d2) val sqx = x * x Math.sqrt(sqx - d1) - Math.sqrt(sqx - d2) val d1 = 0 val d2 = 9 val x = 5 niech: wynik: 5 - 4 = 1 sqx = 25 wynik: 5 - 4 = 1
  8. Przykład 2. Math.sqrt(x * x - d1) - Math.sqrt(x *

    x - d2) val sqx = x * x Math.sqrt(sqx - d1) - Math.sqrt(sqx - d2) val d1 = 0 val d2 = 9 def x = { print("ha"); 5 } niech: wynik: 5 - 4 = 1 "hahahaha" "haha" sqx = 25 wynik: 5 - 4 = 1
  9. Przykład 3. for { n1 <- Future { Thread.sleep(500); 10

    } n2 <- Future { Thread.sleep(1000); 20 } } yield n1 + n2 val f1 = Future { Thread.sleep(500); 10 } val f2 = Future { Thread.sleep(1000); 20 } for { n1 <- f1 n2 <- f2 } yield n1 + n2
  10. 1. Funkcje, które są przezroczyste referencyjnie nazywamy “czystymi” (ang. pure

    functions) 2. Dzięki przezroczystości referencyjnej: a. bezpieczniejsza refaktoryzacja / optymalizacje b. łatwiejsze wnioskowanie 3. Efekty uboczne powodują złamanie przezroczystości referencyjnej!
  11. 1. Wejście / wyjście 2. Utworzenie wątku 3. Interakcja z

    globalnym, mutowalnym stanem 4. Wyjątki 5. Instrukcja return 6. Instrukcja przypisania Źródła efektów ubocznych https://stackoverflow.com/questions/28992625/exceptions-and-referential-transparency https://stackoverflow.com/questions/27800389/does-return-break-referential-transparency
  12. Lokalna mutowalność def sort(list: List[Int]): List[Int] = { val arr

    = list.toArray var i = 1 while(i < arr.length) { var j = i - 1 while(j >= 0) { if (arr(j) > arr(j + 1)) { val temp = arr(j + 1) arr(j + 1) = arr(j) arr(j) = temp } j -= 1 } i += 1 } arr.toList } val xs = List(4,3,6,1,5,2,8,0) foo(sort(xs) ++ bar ) foo(List(0,1,2,3,4,5,6,8) ++ bar) lokalna mutowalność nie łamie przezroczystości referencyjnej
  13. def reverse[A](list: List[A]): List[A] = xs match { case Nil

    => Nil case x :: xs => reverse(xs) :+ x } Jest czysto funkcyjnie! Problemy: 1. Podatność na StackOverflowError 2. Złożoność czasowa O(n2)
  14. def reverse[A](list: List[A], acc: List[A] = Nil): List[A] = list

    match { case Nil => acc case x :: xs => reverse(xs, x :: acc) } reverse(List(1,2,3,4), Nil) = reverse(List(2,3,4), List(1)) = reverse(List(3,4), List(2, 1)) = reverse(List(4), List(3, 2, 1)) = reverse(Nil, List(4, 3, 2, 1)) = List(4, 3, 2, 1) nie robimy nic więcej po wywołaniu rekursywnym
  15. import scala.annotation.tailrec @tailrec def reverse[A](xs: List[A], acc: List[A] = Nil):

    List[A] = xs match { case Nil => acc case x :: xs => reverse(xs, x :: acc) }
  16. Jaki jest problem z rekursją ogonową? 1. Konieczność dodatkowego argumentu

    (tzw. akumulatora) 2. Nie każdą rekursję łatwo da się przerobić na wersję ogonową 3. Rekursja ≈ GOTO
  17. def countBy[A](list: List[A], pred: A => Bool): Int = list

    match { case Nil => 0 case x :: xs if pred(x) => 1 + countBy(xs, pred) case _ :: xs => countBy(xs, pred) } def sumBy[A](list: List[A], f: A => Double): Double = list match { case Nil => 0.0 case x :: xs => f(x) + sumBy(xs, f) }
  18. def sumBy[A](list: List[A], f: A => Int, acc: Double =

    0.0): Double = list match { case Nil => acc case x :: xs => sumBy(xs, f, f(x) + acc) } def countBy[A](list: List[A], pred: A => Bool, acc: Int = 0): Int = list match { case Nil => acc case x :: xs if pred(x) => countBy(xs, pred, 1 + acc) case _ :: xs => countBy(xs, pred, acc) }
  19. trait List[A] { def foldLeft[B](z: B)(op: (B, A) => B):

    B } List(a, b, c).foldLeft(z)(op) → List(b, c).foldLeft(op(z, a))(op) → List(c).foldLeft(op(op(z, a), b))(op) → Nil.foldLeft(op(op(op(z, a), b), c))(op) → op(op(op(z, a), b), c)
  20. def sumBy[A](list: List[A], f: A => Double): Double = list.foldLeft(0.0)

    { case (acc, x) => f(x) + acc } def countBy[A](list: List[A], pred: A => Bool): Int = list.foldLeft(0) { case (acc, x) => if(pred(x)) 1 + acc else acc }
  21. trait List[A] { def foldLeft[B](z: B)(op: (B, A) => B):

    B def scanLeft[B](z: B)(op: (B, A) => B): List[B] def filter(p: A => Boolean): List[A] def map(f: A => B): List[B] def flatMap(f: A => List[B]): List[B] def find(p: A => Boolean): Option[A] def partition(p: A => Boolean): (List[A], List[A]) def groupBy[K](f: A => K): Map[K, List[A]] def zip[B](other: List[B]): List[(A, B)] // ... } funkcje wyższych rzędów to idiomy programowania funkcyjnego
  22. var x = 4 x = x + 2 val

    y = 2 * x + 1 mutowalność utrudnia wnioskowanie
  23. Nazewnictwo vs. mutowalność val weekendDays = Buffer("Piatek", "Sobota", "Niedziela") foo(weekendDays,

    bar) weekendDays.append("Sroda") foo(weekendDays, bar) zapis taki sam, inne znaczenie
  24. Nazewnictwo vs. niemutowalność val weekendDays = List("Piatek", "Sobota", "Niedziela") foo(weekendDays,

    bar) val moreWeekendDays = weekendDays :+ "Sroda" foo(moreWeekendDays, bar) inny zapis, inne znaczenie
  25. Mutowalne referencje do niemutowalnych kolekcji var weekendDays = List("Piatek", "Sobota",

    "Niedziela") foo(weekendDays, bar) weekendDays = weekendDays :+ "Sroda" foo(weekendDays, bar) zapis taki sam, inne znaczenie
  26. Niemutowalność vs. wydajność 1. Kopiowanie jest nieefektywne! 2. Bezpieczne współdzielenie

    3. Większość struktur danych posiada efektywne niemutowalne odpowiedniki 4. Niemutowalne struktury mają swoje osobliwości, np: a. efektywny dostęp do początku listy b. złączanie list w czasie liniowym - wymaga przebudowania pierwszej z nich
  27. 1. Definicja klasy typów to sparametryzowany interfejs trait Show[A] {

    def show(a: A): String } 2. Instancje dla konkretnych typów to implementacje interfejsu implicit val boolShow: Show[Boolean] = (a: Bool) => if(a) "on" else "off" implicit val intShow: Show[Int] = (a: Int) => a.toHexString
  28. 2’. Instancje dla własnych typów case class Foo(bar: Int, baz:

    Bool) object Foo { implicit val fooShow: Show[Foo] = (foo: Foo) => s"""Foo { bar = ${intShow.show(foo.bar)}, baz = ${boolShow.show(foo.baz)} }""" }
  29. 3. Użycie klasy typów to wymaganie dostarczenia implicit parametru def

    log[A](message: String, value: A) (implicit showValue: Show[A]): Unit = { println(message + ": " + showValue.show(value)) } def log[A: Show](message: String, value: A): Unit = { println(message + ": " + implicitly[Show[A]].show(value)) }
  30. Klasy typów - gdzie to jest użyteczne? 1. Serializacja 2.

    Abstrakcje numeryczne 3. Mapowanie schematu bazy danych 4. Generowanie losowych wartości 5. Abstrakcje w teorii kategorii (Functor, Monad, Applicative, etc.) 6. Walidacje strukturalne* 7. Transformacje danych*
  31. Co zyskujemy korzystając z klas typów? 1. Instancja odseparowana od

    definicji typu 2. Możliwość wymiany implementacji w zależności od kontekstu 3. Automatyczna derywacja instancji klas typów*
  32. Derywacja instancji klas typów implicit def pairShow[A: Show, B: Show]:

    Show[(A, B)] = (pair: (A, B)) => { val aStr = implicitly[Show[A]].show(pair._1) val bStr = implicitly[Show[B]].show(pair._2) s"($aStr, $bStr)" } implicitly[Show[(Int, Int)] implicitly[Show[(Bool, Int)] implicitly[Show[(Int, Foo)]
  33. Generyczna derywacja klas typów int bool genTC implicitly[TC[Foo]] string double

    generowanie instancji dla dowolnych typów danych genTC implicitly[TC[Bar]]
  34. Generyczna derywacja klas typów - DYI 1. Magnolia - https://propensive.com/opensource/magnolia/

    2. Shapeless - https://underscore.io/books/shapeless-guide/ 3. Makra (raczej trudne)
  35. Generyczna derywacja klas typów - zalety 1. Odpada nam pisanie

    i utrzymywanie ogromnych ilości kodu 2. Jednolita implementacja wszystkich instancji 3. Wiele bibliotek już teraz oferuje gotowe reguły do generycznej derywacji (wystarczy jeden import i voilà!) 4. Type safety
  36. Generyczna derywacja klas typów - ryzyka 1. Nieumiejętnie używana może

    wydłużyć nam czas kompilacji 2. Niewielki runtime overhead 3. Konieczność zgłębienia nowych abstrakcji 4. Nie zawsze czytelne błędy kompilacji
  37. Problemy z klasycznym I/O 1. Jest efektem ubocznym, a więc

    łamie nam przezroczystość referencyjną 2. Operacje I/O nie są widoczne w systemie typów
  38. Programy jako wartości sealed trait Console[+A] case class Return[A](value: ()

    => A) extends Console[A] case class PrintLine[A](line: String, rest: Console[A]) extends Console[A] case class ReadLine[A](rest: String => Console[A]) extends Console[A] PrintLine("Hello, what is your name?", ReadLine(name => PrintLine(s"Good to meet you, $name") ) )
  39. Programy jako wartości def interpret[A](program: Console[A]): A = program match

    { case Return(value) => value() case PrintLine(line, rest) => println(line); interpret(rest) case ReadLine(rest) => interpret(rest(scala.io.StdIn.readLine())) }
  40. Programy jako wartości val program: Console[String] = for { _

    <- printLine("Hello, what is your name?") name <- readLine _ <- printLine(s"Good to meet you, $name") } yield name
  41. Czysto-funkcyjne I/O 1. Efekty uboczne odroczone do momentu interpretacji 2.

    Przezroczystość referencyjna zachowana 3. Konieczność używania składni monadycznej (for-comprehension)
  42. https://github.com/scalalandio/chimney case class MakeCoffee(id: Int, kind: String, addict: String) case

    class CoffeeMade(id: Int, kind: String, forAddict: String, at: ZonedDateTime) val event = CoffeeMade(id = makeCoffee.id, kind = makeCoffee.kind, forAddict = makeCoffee.addict, at = ZonedDateTime.now)
  43. object Validator { def rule[T](pred: T => Boolean, whenInvalid: String):

    Validator[T] = (obj: T) => if (pred(obj)) Nil else List(ValidationError(whenInvalid)) } https://github.com/krzemin/octopus
  44. case class UserId(id: Int) extends AnyVal case class Email(address: String)

    extends AnyVal case class User(id: UserId, email: Email) implicit val userIdValidator: Validator[UserId] = Validator[UserId] .rule(_.id > 0, "must be positive number") implicit val emailValidator: Validator[Email] = Validator[Email] .rule(_.address.nonEmpty, "must not be empty") .rule(_.address.contains("@"), "must contain @") .rule(_.address.split('@').last.contains("."), "must contain . after @") https://github.com/krzemin/octopus
  45. val user1 = User(UserId(1), Email("[email protected]")) user1.isValid // true user1.validate.toEither //

    Right(user1) val user2 = User(UserId(0), Email("abc@xyz")) user2.isValid // false user2.validate.toEither // Left(List( // (id, must be positive number), // (email, must contain . after @) // )) https://github.com/krzemin/octopus