FP Abstractions

"Functional programming combines the flexibility and power of abstract mathematics with the intuitive clarity of abstract mathematics."

Outline

  • Different flavors, same fundamental concepts
  • What you might already know (RxSomething)
  • map
  • case studies
  • problems with composing "A -> F[B]"
    • -> flatMap
  • common pitfalls when using FP abstractions

A lot of different flavors

Dynamic Typing / Lisps

ML Dialects

A lot of different flavors

OOP/FP Crossover

What you might already know

Observable.from("Hello!", "Hello, world!", "Hello, people!")
  .first(msg => msg.contains("world"))
  .map(msg => msg.toUpperCase())
  .forEach(msg => System.out.println(msg))

// HELLO, WORLD!

RxJava:

"map"

def map[A, B](list: List[A], f: A => B): List[B]
func map<A, B>(list: Array<A>, f: A -> B) -> Array<B>
fun <A, B> map(list: List<A>, f: (A) -> B): List<B>

Scala:

Swift:

Kotlin:

"map"

static <A, B> ArrayList<B> map(ArrayList<A> list, Function<A, B> f)  // <_<'
function map<A, B>(list: Array<A>, f: A => B): Array<B>

Java 8:

Typescript:

Simple example

case class Person(name: String, age: Int)


def map[A, B](list: List[A], f: A => B): List[B]

def filter[A](list: List[A], f: A => Boolean): List[A]
"Find names of underage people:"
val people: List[Person] = ...

val underagePeople = filter(people, isUnderage)

val namesOfUnderagePeople = map(underagePeople, nameOfPerson)
def isUnderage(person: Person): Boolean

def nameOfPerson(person: Person): String

Simple example (OOP + FP)

case class Person(name: String, age: Int)

trait List[A] {
  def map[B](f: A => B): List[B]

  def filter(f: A => Boolean): List[A]
}
"Find names of underage people:"
def isUnderage(person: Person): Boolean

def nameOfPerson(person: Person): String
val people: List[Person] = ...

val underagePeople = people.filter(isUnderage)

val namesOfUnderagePeople = underagePeople.map(nameOfPerson)

Simple example (OOP + FP)

case class Person(name: String, age: Int)

trait List[A] {
  def map[B](f: A => B): List[B]

  def filter(f: A => Boolean): List[A]
}
"Find names of underage people:"
val people: List[Person] = ...

val namesOfUnderagePeople = people
                            .filter(p => p < 18)
                            .map(p => p.name)

Option

trait Option[A]
case class Some(value: A) extends Option[A]
case class None extends Option[A]

Option[A]

 Some(value: A)

 None

Option

trait Option[A] {
  def map[B](f: A => B): Option[B]

  def filter(f: A => Boolean): Option[A]

  def foreach(f: A => Unit): Unit
}
case class Some(value: A) extends Option[A]
case class None extends Option[A]
val optionalString = Option("To be or not to be...")

optionalString
  .map(s => s.replace("...", "!"))    // returns new Some(value = "To be or not to be!")
  .foreach(println)                   // prints "To be or not to be!"
optionalString
  .filter(s => s.length < 5)          // returns None
  .map(s => s.replace("...", "!"))    // doesn't call lambda function; returns None
  .foreach(println)                   // doesn't call println; no return value

Option

trait Option[A] {
  def map[B](f: A => B): Option[B]

  def filter(f: A => Boolean): Option[A]

  def foreach(f: A => Unit): Unit
}
case class Some(value: A) extends Option[A]
case class None extends Option[A]
case class Person(email: String)

def fetchPersonFromDB(id: Int): Option[Person]
fetchPersonFromDB(42)
  .map(person => person.email)              // use email attribute from Person
  .filter(email => email.contains("@"))     // only use email if it's valid
  .foreach(email => sendWelcomeMail(email)) // do something with it

What do we want?

Now!

When do we want it?

Fewer race conditions!

Future (aka Promise/Task)

trait Future[A]
case class Success(value: A) extends Future[A]
case class Failure(exception: Throwable) extends Future[A]
case class Person(name: String, age: Int)

def fetchPersonById(id: Int): Future[Person] = Future { 
  /* make sync network call here */ 
}
val person = fetchPersonById(42)

// print age of person as soon as possible
person
  .map(p => p.age)
  .foreach(age => println(s"The person is $age years old!"))

Error Handling

sealed abstract class Future[A] {
  // used for Success case
  def map[B](f: A => B): Future[B]              

  // used for Failure case
  def recover[B](f: Throwable => B): Future[B] 
  def recoverWith[B](f: Throwable => Future[B]): Future[B]
}
case class Success(value: A) extends Future[A]
case class Failure(exception: Throwable) extends Future[A]
def withRetries[A](retries: Int = 3)(f: => Future[A]): Future[A] = {
  val currentFuture = if (retries > 0) {
    f
  } else {
    Future.failed(new NoRetriesLeftException) // constructs a Failure case
  }

  currentFuture.recoverWith {
    case _: ConnectionTimeoutException => 
      withRetries(retries - 1)(f)             // recurse while decrementing retries

    case e =>                                 // other exceptions are not handled
      Future.failed(e)                        // so we stick with Failure(e)
  }
}

Error Handling

def fetchPersonById(id: String): Future[Person] = Future {
  /* make sync network call here */ 
}

val personFuture: Future[Person] = withRetries() { fetchPersonById(42) }
def fetchPersonById(id: String): Future[Person] = withRetries() {
  Future { 
    /* make sync network call here */ 
  }
}

val personFuture: Future[Person] = fetchPersonById(42)

or

Noticed a pattern?

trait List[A] {
  def map[B](f: A => B): List[B]
}
trait Future[A] {
  def map[B](f: A => B): Future[B]
}
trait Option[A] {
  def map[B](f: A => B): Option[B]
}
trait Mappable[A] {
  def map[B](f: A => B): Mappable[B]
}

It's a pattern!

 (actually called "Functor")

trait Functor[A] {
  def map[B](f: A => B): Functor[B]
}

Many types can be Functors

Type Abstracts over
List Number of elements
Option Presence of value
Future Concurrent computations (also failure)
Either Two distinct cases (mostly for error handling)
Try Exceptions (try-catch as Functor)
Observable Events over time (also concurrency, failure)
ParSeq Parallel transformations (multiple cores!)
RDD Clustering of data and computations

Observable is the "deep end"

One Multiple
Sync A List[A]
Async Future[A] Observable[A]

This is awesome!

Let's use it for something more ambitious...

def findPersonByName(name: String): Future[Person]

def subscriptionForPerson(person: Person): Future[Subscription]

def fetchContent(hasActiveSubscription: Boolean): Future[Content]
val person: Future[Person] = findPersonByName("Joe")
val subscription: Future[Future[Subscription]] = person.map(p => subscriptionForPerson(p))
val content: Future[Future[Future[Content]]] = 
  subscription.map(s => s.map(s1 => fetchContent(s1.isActive)))
trait Future[A] {
  def map[B](f: A => B): Future[B]
}

// Future[A]                    => Future[B]
// Future[Person]               => Future[Future[Subscription]]
// Future[Future[Subscription]] => Future[Future[Future[Content]]]

WTF is going on?

Welp, that's no good... :(

How do we solve this?

def findPersonByName(name: String): Future[Person]               // A => F[B]

def subscriptionForPerson(person: Person): Future[Subscription]  // A => F[B]
trait Future[A] {
  def map[B](f: A => B): Future[B]
}

// Future[A]                    => Future[B]
// Future[Person]               => Future[Future[Subscription]

Instead of...

trait Future[A] {
  def mapWithoutNestingPlz[B](f: A => Future[B]): Future[B]
}

// Future[A]                    => Future[B]
// Future[Person]               => Future[Subscription]

...we want something like this:

(let's call it "flatMap")

trait Future[A] {
  def flatMap[B](f: A => Future[B]): Future[B]
}

// Future[A]                    => Future[B]
// Future[Person]               => Future[Subscription]

Let's try again

def findPersonByName(name: String): Future[Person]

def subscriptionForPerson(person: Person): Future[Subscription]

def fetchContent(hasActiveSubscription: Boolean): Future[Content]
val person: Future[Person] = findPersonByName("Joe")
val subscription: Future[Subscription] = person.flatMap(p => subscriptionForPerson(p))
val content: Future[Content] = 
  subscription.flatMap(s => fetchContent(subscription.isActive))

That's better :)

Psst, another pattern...

trait List[A] {
  def flatMap[B](f: A => List[B]): List[B]
}
trait Future[A] {
  def flatMap[B](f: A => Future[B]): Future[B]
}
trait Option[A] {
  def flatMap[B](f: A => Option[B): Option[B]
}
trait FlatMappable[A] {
  def flatMap[B](f: A => FlatMappable[B]): FlatMappable[B]
}

Yay, so flat!

 (actually called "Monad")

trait Monad[A] {
  def flatMap[B](f: A => Monad[B]): Monad[B]
}

Common pitfalls

1: Trying to directly use the inner value

object NamesDatabase {
  def getNameById(id: Int): Option[String]
}

val optionalName: Option[String] = NamesDatabase.getNameById(21)
// DON'T force unwrap optionals!
val lowercaseName: String = optionalName.get.toLowerCase  // BOOM!
// DO use map instead:
val lowercaseName: Option[String] = optionalName.map(name => name.toLowerCase)
// DO provide a default value to handle the "None" case
val lowercaseName: String = optionalName.getOrElse("Anonymous").toLowerCase

or

Common pitfalls

2: Jumping in and out of an abstraction

def findPersonByName(name: String): Future[Person]

def subscriptionForPerson(id: Person): Future[Subscription]

def fetchContent(hasActiveSubscription: Boolean): Future[Content]
// DON'T jump in and out of an abstraction
val personFuture: Future[Person] = findPersonByName("Joe")
val person: Person = Await.result(personFuture, 30.seconds)

val subscriptionFuture: Future[Subscription] = subscriptionForPerson(person)
val subscription: Subscription = Await.result(subscriptionFuture, 30.seconds)

val contentFuture: Future[Content] = fetchContent(subscription.isActive)
val content: Content = Await.result(contentFuture, 30.seconds)
// DO use map, flatMap and other combinators instead:
val contentFuture = findPersonByName("Joe")
  .flatMap(person => subscriptionById(person.id))
  .flatMap(subscription => fetchContent(subscription.active))

contentFuture.foreach(content => doSomethingWith(content))

contentFuture.failed.foreach(exception => somehowHandle(exception))

Common pitfalls

3: Nesting similar abstractions

// DON'T do this
def tagsForRecipe(recipe: Recipe): Option[List[Tag]]]

// using the result
tagsForRecipe(someRecipe).map { tagList =>
  tagList.foreach { tag =>
    // do something with each tag
  }
}
// DO this instead
def tagsForRecipe(recipe: Recipe): List[Tag]

// using the result
tagsForRecipe(someRecipe).foreach { tag =>
  // do something with each tag
}

Common pitfalls

4: Transformations on a "higher" level than necessary

// DON'T write functions on container types if you just need the inner value
def countOptionalWords(text: Option[String]): Option[Int] = {
  if (text.isDefined) {        // or: text.map(t => t.split(" ").size)
    text.get.split(" ").size 
  } else {
    None
  }
}
// DO write functions that transform simple values and map/flatMap them
def countWords(text: String): Int = {
  text.split(" ").size
}

someOptionalText.map(t => countWords(t))  // Option[String] => Option[Int]

Common pitfalls

5: Not handling everything when leaving an abstraction

def fetchContent(id: Int): Future[Content]
// DON'T try to use the inner value directly, it might not be there yet and blow up
val content = fetchContent(42).value.get  // BOOM
// DON'T block indefinitely for something calculated concurrently
val content = fetchContent(42).waitThenGet  // Might result in deadlocks
// IF you need to leave the abstraction, handle everything the abstraction does for you
val contentFuture = fetchContent(42)

Await.result(contentFuture, 30.seconds) match {
  case Success(content)         => writeToFile(content)
  case Failure(_: NetworkError) => Log.warn(s"There was a network error")
  case Failure(exc)             => Log.exception(exc)
}
// DON'T ignore failure states when leaving abstractions!
fetchContent(42).foreach(writeToFile)  // Swallows failures silently

Questions?

Made with Slides.com