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): StringSimple 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): Stringval 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 itWhat 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").toLowerCaseor
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 silentlyQuestions?
FP Abstractions
By Felix Bruckmeier
FP Abstractions
- 156