FP in Scala into the rabbit hole (part 1)
Introduction ● This is mostly about Functional programming ● This is not about Scala language, but we will go through them when we are run into language features ● A lot of my examples here comes from the book Functional programming in Scala
Functional Programming One famous Chinese saying said: As small the sparrow is, it has all the vital organs a life needs. Let begin with a small “Sparrow”
List (Singly linked) Imperative version is more mutable: 1 2 3 4 Functional version is more immutable/recursive FP List 1 2 3 4 2 3 4 3 4
Algebraic Data Type We model List as one Algebraic Data Type In scala ADT are modeled mostly as sealed trait and case classes extends from it. Look at the next slide
pure FP List sealed trait List[+A] case object Nil extends List[Nothing] case class Cons[+A] (head: A, tail: List[A]) extends List[A] object List { def apply[A](as: A*): List[A] = if (as.isEmpty) Nil else Cons(as.head, apply(as.tail: _*)) private def asString_internal[A](l: List[A]): String = l match { case Nil => "" case Cons(head,tail) => head.toString + " " + asString_internal(tail) } def toString[A](l: List[A]): String = "[ " + asString_internal(l) + "]" }
traits traits in scala is similar to mixins in Ruby It is contains certain functionality/interface that can be inherited and used by classes, can have both concrete methods and abstract methods. sealed traits means all the class inherit this trait must be in the same file trait in. This give compiler the power to know the “full” set of classes inherit the trait.
case class case class is the special class can be used in functional programming pattern matching mechanism. sealed traits and case class are the standard way in scala to model Algebraic Data Type, which will be the fundamental building blocks of functional programming
companion object object in scala is basically singleton object in other language. You can have a companion object with same name of the class/trait in scala, companion object can have access of the private method/data of the class/trait it associated to. Here we mostly use it as a module for the ADT.
List ADT sealed trait List[+A] case object Nil extends List[Nothing] case class Cons[+A] (head: A, tail: List[A]) extends List[A] Here we define List as a trait, and 2 type of lists are defined, Nil as empty List, and Cons as non-empty List.
Type Variance sealed trait List[+A] A is just abstract type (generic) +A here is called covariant, means that if A` is A’s subtype, then List[A`] is List[A]’s subtype.
Constructor object List { def apply[A](as: A*): List[A] = if (as.isEmpty) Nil else Cons(as.head, apply(as.tail: _*)) … You can create constructor for the List trait with different signatures here, you can have multiple constructor here in companion object.
Pattern Matching private def asString_internal[A](l: List[A]): String = l match { case Nil => "" case Cons(head,tail) => head.toString + " " + asString_internal(tail) } def toString[A](l: List[A]): String = "[ " + asString_internal(l) + "]" } Now we define toString function here, we here use pattern matching to see what is the remaining list type between Nil and Cons. Pattern Matching is a very powerful feature in FP, and it can match into the multiple layers of objects, match types and much more.
Recursive You can also notice that we use recursive here, recursive is preferred in FP to loops. And will avoid mutables. In scala as it is based on Java ByteCode, it is still possible to stack overflow, so we sometime will need to try to do tail recursion as much as possible, we will talk about it later.
Scala is not Pure FP As we can see in early slide, we now have a minimum functional List, it does not really use OO concepts (trait and case class here only use to model ADT). This is kind of pure FP way to do the function programming: A few types and functions that work on the types. If you look at scala doc for their List, it kind of different and toString is directly a method of List. It is in fact more OO/FP hybride way to do it. Let change to that and see.
OO/FP List sealed trait List[+A] { def head: A def tail: List[A] def isEmpty: Boolean override def toString = "[ " + asString_internal(this) + "]" private def asString_internal[B >: A](l: List[B]): String = l match { case Cons(head,tail) => head.toString + " " + asString_internal(tail) case _ => "" } } object List { def apply[A](as: A*): List[A] = if (as.isEmpty) Nil else Cons(as.head, apply(as.tail: _*)) } case object Nil extends List[Nothing] { override def isEmpty = true override def head: Nothing = throw new NoSuchElementException("head of empty list") override def tail: List[Nothing] = throw new UnsupportedOperationException("tail of empty list") } case class Cons[+A] (hd: A, tl: List[A]) extends List[A] { override def isEmpty = false override def head : A = hd override def tail : List[A] = tl }
lower bound (Type variation) asString_internal[B >: A](l: List[B]): String Here the B >: A is called lower bound in type variation, which means that, B must be a supertype of A. This is required to satisfy the type variance check (you can not use A here directly in place of B) check this
More List functions def sum(ints: List[Int]): Int = ints match { case Nil => 0 case Cons(x,xs) => x + sum(xs) } def product(ds: List[Double]): Double = ds match { case Nil => 1.0 case Cons(0.0, _) => 0.0 case Cons(x,xs) => x * product(xs) } These 2 common functions seems really similar, how we can generalize it.
foldRight def foldRight[A,B](l: List[A], z: B)(f: (A, B) => B): B = l match { case Nil => z case Cons(x, xs) => f(x, foldRight(xs, z)(f)) } def sum(l: List[Int]) = foldRight(l, 0)((x,y) => x + y) def product(l: List[Double]) = foldRight(l, 1.0)(_ * _) Here is the higher order function (function take function as parameter or output), a very power/interesting feature in FP. Here _ * _ means the first parameter * second parameter, which is a short hand way to write it.
tail recursion foldRight is abstract and powerful, but it can stack overflow, what can we do? @annotation.tailrec def foldLeft[A,B](l: List[A], z: B)(f: (B, A) => B): B = l match { case Nil => z case Cons(h,t) => foldLeft(t, f(z,h))(f) } def reverse[A](l: List[A]): List[A] = foldLeft(l, List[A]())((acc,h) => Cons(h,acc)) def foldRight[A,B](l: List[A], z: B)(f: (A,B) => B): B = foldLeft(reverse(l), z)((b,a) => f(a,b)) foldLeft is tail recursive, and it enforced (by the annotation) to optimize into loop in background, then we can use this FoldLeft to implement FoldRight (now no stack overflow
map You should be familiar with map on List def map[A, B](la: List[A])(f: A => B): List[B] = foldRight(la, Nil:List[B])((h,t) => Cons(f(h), t)) val a = List(1,2,3,4) List.map(a)(_+1) we get List(2,3,4,5)
There are far more than map def unit[A](a: A): List[A] = Cons(a, Nil) def flatMap[A, B](la: List[A])(f: A => List[B]): List[B] = concat(foldRight(la, Nil:List[List[B]])((h,t) => Cons(f(h),t))) def join[A](lla: List[List[A]]): List[A] = flatMap(lla)(la => la) def compose[A,B,C](f: A => List[B], g: B => List[C]): A => List[C] = a => flatMap(f(a))(g) def apply[A,B](la: List[A])(f: List[A => B]): List[B] = flatMap(f)(t1 => flatMap(la)(t2 => unit(t1(t2)))) def map2[A,B,C](la: List[A], lb: List[B])(f: (A, B) => C): List[C] = apply(lb)(apply(la)(unit(f.curried))) def map[A, B](la: List[A])(f: A => B): List[B] = apply(la)(unit(f)) def fold[A](l: List[A])(z: A)(op: (A, A) ⇒ A): A = foldRight(l, z)(op) def filter[A](l: List[A])(f: A => Boolean): List[A] = foldRight(l, Nil:List[A])((h,t) => if (f(h)) Cons(h,t) else t)
What are they You can see, except for unit[A](a: A): List[A] fold[A](l: List[A])(z: A)(op: (A, A) ⇒ A): A filter[A](l: List[A])(f: A => Boolean): List[A] There are 3 groups: 1. flatMap/join/compose 2. apply/map2 3. map inside each group, the methods are equivalent to each other. G1 => G2 => G3, which means G1 can implement G2, G2 can implement G3. Powerful G1 > G2 > G3 Generality G1 < G2 < G3 OO sense, G1 inherit from G2 inherit from G3
What are they G1 is Monad Signature G2 is Applicative Signature G3 is Functor Signature fold is the signature of Foldable Monad with filter is called Zero Monad It is easier to see, but much harder to understand. What they are. Why we use them. What can they do. Once you understand, you lost the ability to say it :) We will find out that through our journey. Here we can have a small taste first.
Scala for comprehension Any type that has flatMap/map/filter defined on them can use for comprehension. Let us put those in OO/FP hybrid style
for comprehension Use the OO/FP hybrid style code we can do def test = { val a = List(1,2) val b = List(3,4) for { t1 <- a t2 <- b } yield ((t1,t2)) } We get [ (1,3) (1,4) (2,3) (2,4) ]
for comprehension It is in fact flatMap/map in disguise, it is same as def test = { val a = List(1,2) val b = List(3,4) a.flatMap { t1 => b.map { t2 => (t1,t2) } } } basically multiple List can combine together in any ways, without knowing what inside, the last one is map, all the other are flatMap, you can also use if here in for comprehension, which translate into filter

Fp in scala part 1

  • 1.
    FP in Scala into the rabbit hole (part 1)
  • 2.
    Introduction ● Thisis mostly about Functional programming ● This is not about Scala language, but we will go through them when we are run into language features ● A lot of my examples here comes from the book Functional programming in Scala
  • 3.
    Functional Programming Onefamous Chinese saying said: As small the sparrow is, it has all the vital organs a life needs. Let begin with a small “Sparrow”
  • 4.
    List (Singly linked) Imperative version is more mutable: 1 2 3 4 Functional version is more immutable/recursive FP List 1 2 3 4 2 3 4 3 4
  • 5.
    Algebraic Data Type We model List as one Algebraic Data Type In scala ADT are modeled mostly as sealed trait and case classes extends from it. Look at the next slide
  • 6.
    pure FP List sealed trait List[+A] case object Nil extends List[Nothing] case class Cons[+A] (head: A, tail: List[A]) extends List[A] object List { def apply[A](as: A*): List[A] = if (as.isEmpty) Nil else Cons(as.head, apply(as.tail: _*)) private def asString_internal[A](l: List[A]): String = l match { case Nil => "" case Cons(head,tail) => head.toString + " " + asString_internal(tail) } def toString[A](l: List[A]): String = "[ " + asString_internal(l) + "]" }
  • 7.
    traits traits inscala is similar to mixins in Ruby It is contains certain functionality/interface that can be inherited and used by classes, can have both concrete methods and abstract methods. sealed traits means all the class inherit this trait must be in the same file trait in. This give compiler the power to know the “full” set of classes inherit the trait.
  • 8.
    case class caseclass is the special class can be used in functional programming pattern matching mechanism. sealed traits and case class are the standard way in scala to model Algebraic Data Type, which will be the fundamental building blocks of functional programming
  • 9.
    companion object objectin scala is basically singleton object in other language. You can have a companion object with same name of the class/trait in scala, companion object can have access of the private method/data of the class/trait it associated to. Here we mostly use it as a module for the ADT.
  • 10.
    List ADT sealedtrait List[+A] case object Nil extends List[Nothing] case class Cons[+A] (head: A, tail: List[A]) extends List[A] Here we define List as a trait, and 2 type of lists are defined, Nil as empty List, and Cons as non-empty List.
  • 11.
    Type Variance sealedtrait List[+A] A is just abstract type (generic) +A here is called covariant, means that if A` is A’s subtype, then List[A`] is List[A]’s subtype.
  • 12.
    Constructor object List{ def apply[A](as: A*): List[A] = if (as.isEmpty) Nil else Cons(as.head, apply(as.tail: _*)) … You can create constructor for the List trait with different signatures here, you can have multiple constructor here in companion object.
  • 13.
    Pattern Matching privatedef asString_internal[A](l: List[A]): String = l match { case Nil => "" case Cons(head,tail) => head.toString + " " + asString_internal(tail) } def toString[A](l: List[A]): String = "[ " + asString_internal(l) + "]" } Now we define toString function here, we here use pattern matching to see what is the remaining list type between Nil and Cons. Pattern Matching is a very powerful feature in FP, and it can match into the multiple layers of objects, match types and much more.
  • 14.
    Recursive You canalso notice that we use recursive here, recursive is preferred in FP to loops. And will avoid mutables. In scala as it is based on Java ByteCode, it is still possible to stack overflow, so we sometime will need to try to do tail recursion as much as possible, we will talk about it later.
  • 15.
    Scala is notPure FP As we can see in early slide, we now have a minimum functional List, it does not really use OO concepts (trait and case class here only use to model ADT). This is kind of pure FP way to do the function programming: A few types and functions that work on the types. If you look at scala doc for their List, it kind of different and toString is directly a method of List. It is in fact more OO/FP hybride way to do it. Let change to that and see.
  • 16.
    OO/FP List sealedtrait List[+A] { def head: A def tail: List[A] def isEmpty: Boolean override def toString = "[ " + asString_internal(this) + "]" private def asString_internal[B >: A](l: List[B]): String = l match { case Cons(head,tail) => head.toString + " " + asString_internal(tail) case _ => "" } } object List { def apply[A](as: A*): List[A] = if (as.isEmpty) Nil else Cons(as.head, apply(as.tail: _*)) } case object Nil extends List[Nothing] { override def isEmpty = true override def head: Nothing = throw new NoSuchElementException("head of empty list") override def tail: List[Nothing] = throw new UnsupportedOperationException("tail of empty list") } case class Cons[+A] (hd: A, tl: List[A]) extends List[A] { override def isEmpty = false override def head : A = hd override def tail : List[A] = tl }
  • 17.
    lower bound (Typevariation) asString_internal[B >: A](l: List[B]): String Here the B >: A is called lower bound in type variation, which means that, B must be a supertype of A. This is required to satisfy the type variance check (you can not use A here directly in place of B) check this
  • 18.
    More List functions def sum(ints: List[Int]): Int = ints match { case Nil => 0 case Cons(x,xs) => x + sum(xs) } def product(ds: List[Double]): Double = ds match { case Nil => 1.0 case Cons(0.0, _) => 0.0 case Cons(x,xs) => x * product(xs) } These 2 common functions seems really similar, how we can generalize it.
  • 19.
    foldRight def foldRight[A,B](l:List[A], z: B)(f: (A, B) => B): B = l match { case Nil => z case Cons(x, xs) => f(x, foldRight(xs, z)(f)) } def sum(l: List[Int]) = foldRight(l, 0)((x,y) => x + y) def product(l: List[Double]) = foldRight(l, 1.0)(_ * _) Here is the higher order function (function take function as parameter or output), a very power/interesting feature in FP. Here _ * _ means the first parameter * second parameter, which is a short hand way to write it.
  • 20.
    tail recursion foldRightis abstract and powerful, but it can stack overflow, what can we do? @annotation.tailrec def foldLeft[A,B](l: List[A], z: B)(f: (B, A) => B): B = l match { case Nil => z case Cons(h,t) => foldLeft(t, f(z,h))(f) } def reverse[A](l: List[A]): List[A] = foldLeft(l, List[A]())((acc,h) => Cons(h,acc)) def foldRight[A,B](l: List[A], z: B)(f: (A,B) => B): B = foldLeft(reverse(l), z)((b,a) => f(a,b)) foldLeft is tail recursive, and it enforced (by the annotation) to optimize into loop in background, then we can use this FoldLeft to implement FoldRight (now no stack overflow
  • 21.
    map You shouldbe familiar with map on List def map[A, B](la: List[A])(f: A => B): List[B] = foldRight(la, Nil:List[B])((h,t) => Cons(f(h), t)) val a = List(1,2,3,4) List.map(a)(_+1) we get List(2,3,4,5)
  • 22.
    There are farmore than map def unit[A](a: A): List[A] = Cons(a, Nil) def flatMap[A, B](la: List[A])(f: A => List[B]): List[B] = concat(foldRight(la, Nil:List[List[B]])((h,t) => Cons(f(h),t))) def join[A](lla: List[List[A]]): List[A] = flatMap(lla)(la => la) def compose[A,B,C](f: A => List[B], g: B => List[C]): A => List[C] = a => flatMap(f(a))(g) def apply[A,B](la: List[A])(f: List[A => B]): List[B] = flatMap(f)(t1 => flatMap(la)(t2 => unit(t1(t2)))) def map2[A,B,C](la: List[A], lb: List[B])(f: (A, B) => C): List[C] = apply(lb)(apply(la)(unit(f.curried))) def map[A, B](la: List[A])(f: A => B): List[B] = apply(la)(unit(f)) def fold[A](l: List[A])(z: A)(op: (A, A) ⇒ A): A = foldRight(l, z)(op) def filter[A](l: List[A])(f: A => Boolean): List[A] = foldRight(l, Nil:List[A])((h,t) => if (f(h)) Cons(h,t) else t)
  • 23.
    What are they You can see, except for unit[A](a: A): List[A] fold[A](l: List[A])(z: A)(op: (A, A) ⇒ A): A filter[A](l: List[A])(f: A => Boolean): List[A] There are 3 groups: 1. flatMap/join/compose 2. apply/map2 3. map inside each group, the methods are equivalent to each other. G1 => G2 => G3, which means G1 can implement G2, G2 can implement G3. Powerful G1 > G2 > G3 Generality G1 < G2 < G3 OO sense, G1 inherit from G2 inherit from G3
  • 24.
    What are they G1 is Monad Signature G2 is Applicative Signature G3 is Functor Signature fold is the signature of Foldable Monad with filter is called Zero Monad It is easier to see, but much harder to understand. What they are. Why we use them. What can they do. Once you understand, you lost the ability to say it :) We will find out that through our journey. Here we can have a small taste first.
  • 25.
    Scala for comprehension Any type that has flatMap/map/filter defined on them can use for comprehension. Let us put those in OO/FP hybrid style
  • 26.
    for comprehension Usethe OO/FP hybrid style code we can do def test = { val a = List(1,2) val b = List(3,4) for { t1 <- a t2 <- b } yield ((t1,t2)) } We get [ (1,3) (1,4) (2,3) (2,4) ]
  • 27.
    for comprehension Itis in fact flatMap/map in disguise, it is same as def test = { val a = List(1,2) val b = List(3,4) a.flatMap { t1 => b.map { t2 => (t1,t2) } } } basically multiple List can combine together in any ways, without knowing what inside, the last one is map, all the other are flatMap, you can also use if here in for comprehension, which translate into filter