"Functional programming combines the flexibility and power of abstract mathematics with the intuitive clarity of abstract mathematics."
Dynamic Typing / Lisps
ML Dialects
OOP/FP Crossover
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:
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:
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:
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): Stringcase 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)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)trait Option[A]
case class Some(value: A) extends Option[A]
case class None extends Option[A]Option[A]
Some(value: A)
None
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
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!
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!"))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)
}
}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
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]
}| 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 |
| One | Multiple | |
|---|---|---|
| Sync | A | List[A] |
| Async | Future[A] | Observable[A] |
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]]]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]trait Future[A] {
def mapWithoutNestingPlz[B](f: A => Future[B]): Future[B]
}
// Future[A] => Future[B]
// Future[Person] => Future[Subscription]trait Future[A] {
def flatMap[B](f: A => Future[B]): Future[B]
}
// Future[A] => Future[B]
// Future[Person] => Future[Subscription]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 :)
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]
}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").toLowerCasedef 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))// 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
}// 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]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