The Terror-Free Guide to
Introducing Functional Scala at Work
(without dying in the process)
JORGE VÁSQUEZ
SCALA DEVELOPER
We are hiring Scala Developers!
https://scalac.io/careers/
Agenda
● Motivation
● ZIO Prelude Overview
● How ZIO Prelude can help us
Motivation:
Boilerplate
Example 1
Let's suppose we have some cache stats data, organized
by date and application. This data comes from two
sources, and we need to combine them into one, by
summing counters when collisions exist.
Example 1
Solution 1
final case class CacheStats(
entryCount: Option[Int],
memorySize: Option[Long],
hits: Option[Long],
misses: Option[Long],
loads: Option[Long],
evictions: Option[Long]
)
object CacheStats {
def make(
entryCount: Option[Int],
memorySize: Option[Long],
hits: Option[Long],
misses: Option[Long],
loads: Option[Long],
evictions: Option[Long]
): CacheStats = CacheStats(entryCount, memorySize, hits, misses, loads, evictions)
}
Solution 1
pprint.pprintln(stats1 ++ stats2)
/*
Map(
2020-01-18 -> Map(
"App X" -> CacheStats(None, Some(5000L), Some(2000L),
Some(500L), Some(5000L), Some(2500L)),
"App Y" -> CacheStats(Some(800), None, Some(3100L), None,
Some(7890L), Some(1513L)),
"App Z" -> CacheStats(None, Some(678L), None, None, Some(800L),
None)
),
2020-01-19 -> Map(
"App A" -> CacheStats(None, None, Some(4098L), None,
Some(5418L), None),
"App B" -> CacheStats(Some(1567), None, Some(4098L),
Some(1000L), Some(5418L), Some(3000L)),
"App C" -> CacheStats(None, None, Some(500L), Some(467L),
Some(800L), None)
),
2020-03-05 -> Map(
"App A" -> CacheStats(Some(4378), None, Some(3210L),
Some(1000L), None, None),
"App Y" -> CacheStats(None, Some(1345L), Some(9032L),
Some(123L), None, None)
),
2020-04-10 -> Map("App X" -> CacheStats(None, None, Some(432L),
None, Some(2541L), None))
)
*/
val stats1: Map[LocalDate, Map[String, CacheStats]] = Map(
LocalDate.of(2020, 1, 18) -> Map(
"App X" -> CacheStats.make(Some(1000), Some(5000), Some(2000), Some(500), Some(5000), Some(2500)),
"App Y" -> CacheStats.make(None, Some(3500), Some(3100), None, Some(7890), Some(1513)),
"App Z" -> CacheStats.make(None, None, Some(500), None, Some(800), None)
),
LocalDate.of(2020, 1, 19) -> Map(
"App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None),
"App B" -> CacheStats.make(Some(1567), Some(2854), Some(4098), Some(1000), Some(5418), Some(3000)),
"App C" -> CacheStats.make(None, None, Some(500), None, Some(800), None)
),
LocalDate.of(2020, 3, 5) -> Map(
"App A" -> CacheStats.make(Some(4378), Some(1000), Some(3210), Some(1000), None, None),
"App Y" -> CacheStats.make(None, None, Some(9032), Some(123), None, None)
),
LocalDate.of(2020, 4, 10) -> Map(
"App X" -> CacheStats.make(Some(1879), None, Some(432), None, Some(2541), None)
)
)
val stats2: Map[LocalDate, Map[String, CacheStats]] = Map(
LocalDate.of(2020, 1, 18) -> Map(
"App X" -> CacheStats.make(None, Some(5000), Some(2000), Some(500), Some(5000), Some(2500)),
"App Y" -> CacheStats.make(Some(800), None, Some(3100), None, Some(7890), Some(1513)),
"App Z" -> CacheStats.make(None, Some(678), None, None, Some(800), None)
),
LocalDate.of(2020, 1, 19) -> Map(
"App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None),
"App B" -> CacheStats.make(Some(1567), None, Some(4098), Some(1000), Some(5418), Some(3000)),
"App C" -> CacheStats.make(None, None, Some(500), Some(467), Some(800), None)
),
LocalDate.of(2020, 3, 5) -> Map(
"App A" -> CacheStats.make(Some(4378), None, Some(3210), Some(1000), None, None),
"App Y" -> CacheStats.make(None, Some(1345), Some(9032), Some(123), None, None)
),
LocalDate.of(2020, 4, 10) -> Map(
"App X" -> CacheStats.make(None, None, Some(432), None, Some(2541), None)
)
)
Solution 1
Problems with Solution 1
Solution 2
final case class CacheStats(
entryCount: Option[Int],
memorySize: Option[Long],
hits: Option[Long],
misses: Option[Long],
loads: Option[Long],
evictions: Option[Long]
) { self =>
def combine(that: CacheStats): CacheStats = {
def combineCounters[A: Numeric](left: Option[A], right: Option[A]): Option[A] =
(left, right) match {
case (Some(l), Some(r)) => Some(implicitly[Numeric[A]].plus(l, r))
case (Some(l), None) => Some(l)
case (None, Some(r)) => Some(r)
case (None, None) => None
}
CacheStats(
combineCounters(self.entryCount, that.entryCount),
combineCounters(self.memorySize, that.memorySize),
combineCounters(self.hits, that.hits),
combineCounters(self.misses, that.misses),
combineCounters(self.loads, that.loads),
combineCounters(self.evictions, that.evictions)
)
}
}
Solution 2
def combine(
left: Map[LocalDate, Map[String, CacheStats]],
right: Map[LocalDate, Map[String, CacheStats]]
): Map[LocalDate, Map[String, CacheStats]] = {
(left.keySet ++ right.keySet).map { date =>
val newStatsByApp = (left.get(date), right.get(date)) match {
case (Some(v1), None) => v1
case (None, Some(v2)) => v2
case (Some(v1), Some(v2)) =>
(v1.keySet ++ v2.keySet).map { location =>
val newStats = (v1.get(location), v2.get(location)) match {
case (Some(s1), None) => s1
case (None, Some(s2)) => s2
case (Some(s1), Some(s2)) => s1 combine s2
case (None, None) => throw new Error("Unexpected scenario")
}
location -> newStats
}.toMap
case (None, None) => throw new Error("Unexpected scenario")
}
date -> newStatsByApp
}
}.toMap
Solution 2
pprint.pprintln(combine(stats1, stats2))
/*
Map(
2020-01-18 -> Map(
"App X" -> CacheStats(Some(1000), Some(10000L), Some(4000L),
Some(1000L), Some(10000L), Some(5000L)),
"App Y" -> CacheStats(Some(800), Some(3500L), Some(6200L), None,
Some(15780L), Some(3026L)),
"App Z" -> CacheStats(None, Some(678L), Some(500L), None, Some(1600L),
None)
),
2020-01-19 -> Map(
"App A" -> CacheStats(None, None, Some(8196L), None, Some(10836L),
None),
"App B" -> CacheStats(Some(3134), Some(2854L), Some(8196L),
Some(2000L), Some(10836L), Some(6000L)),
"App C" -> CacheStats(None, None, Some(1000L), Some(467L), Some(1600L),
None)
),
2020-03-05 -> Map(
"App A" -> CacheStats(Some(8756), Some(1000L), Some(6420L),
Some(2000L), None, None),
"App Y" -> CacheStats(None, Some(1345L), Some(18064L), Some(246L),
None, None)
),
2020-04-10 -> Map("App X" -> CacheStats(Some(1879), None, Some(864L),
None, Some(5082L), None))
)
*/
val stats1: Map[LocalDate, Map[String, CacheStats]] = Map(
LocalDate.of(2020, 1, 18) -> Map(
"App X" -> CacheStats.make(Some(1000), Some(5000), Some(2000), Some(500), Some(5000), Some(2500)),
"App Y" -> CacheStats.make(None, Some(3500), Some(3100), None, Some(7890), Some(1513)),
"App Z" -> CacheStats.make(None, None, Some(500), None, Some(800), None)
),
LocalDate.of(2020, 1, 19) -> Map(
"App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None),
"App B" -> CacheStats.make(Some(1567), Some(2854), Some(4098), Some(1000), Some(5418), Some(3000)),
"App C" -> CacheStats.make(None, None, Some(500), None, Some(800), None)
),
LocalDate.of(2020, 3, 5) -> Map(
"App A" -> CacheStats.make(Some(4378), Some(1000), Some(3210), Some(1000), None, None),
"App Y" -> CacheStats.make(None, None, Some(9032), Some(123), None, None)
),
LocalDate.of(2020, 4, 10) -> Map(
"App X" -> CacheStats.make(Some(1879), None, Some(432), None, Some(2541), None)
)
)
val stats2: Map[LocalDate, Map[String, CacheStats]] = Map(
LocalDate.of(2020, 1, 18) -> Map(
"App X" -> CacheStats.make(None, Some(5000), Some(2000), Some(500), Some(5000), Some(2500)),
"App Y" -> CacheStats.make(Some(800), None, Some(3100), None, Some(7890), Some(1513)),
"App Z" -> CacheStats.make(None, Some(678), None, None, Some(800), None)
),
LocalDate.of(2020, 1, 19) -> Map(
"App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None),
"App B" -> CacheStats.make(Some(1567), None, Some(4098), Some(1000), Some(5418), Some(3000)),
"App C" -> CacheStats.make(None, None, Some(500), Some(467), Some(800), None)
),
LocalDate.of(2020, 3, 5) -> Map(
"App A" -> CacheStats.make(Some(4378), None, Some(3210), Some(1000), None, None),
"App Y" -> CacheStats.make(None, Some(1345), Some(9032), Some(123), None, None)
),
LocalDate.of(2020, 4, 10) -> Map(
"App X" -> CacheStats.make(None, None, Some(432), None, Some(2541), None)
)
)
Example 2
Let's suppose we have a List of cache stats data, and we
want to sum all stats.
Solution
final case class CacheStats(
entryCount: Option[Int],
memorySize: Option[Long],
hits: Option[Long],
misses: Option[Long],
loads: Option[Long],
evictions: Option[Long]
) { self =>
def combine(that: CacheStats): CacheStats = {
def combineCounters[A: Numeric](left: Option[A], right: Option[A]): Option[A] =
(left, right) match {
case (Some(l), Some(r)) => Some(implicitly[Numeric[A]].plus(l, r))
case (Some(l), None) => Some(l)
case (None, Some(r)) => Some(r)
case (None, None) => None
}
CacheStats(
combineCounters(self.entryCount, that.entryCount),
combineCounters(self.memorySize, that.memorySize),
combineCounters(self.hits, that.hits),
combineCounters(self.misses, that.misses),
combineCounters(self.loads, that.loads),
combineCounters(self.evictions, that.evictions)
)
}
}
Solution
def sum(cacheStats: List[CacheStats]): CacheStats =
cacheStats.foldRight(CacheStats.make(None, None, None, None, None, None))(_ combine _)
def main(args: Array[String]): Unit = {
val cacheStats1 = List(
CacheStats.make(Some(1000), Some(5000), Some(2000), Some(500), Some(5000), Some(2500)),
CacheStats.make(None, Some(3500), Some(3100), None, Some(7890), Some(1513)),
CacheStats.make(None, None, Some(500), None, Some(800), None)
)
val cacheStats2 = List.empty[CacheStats]
println(sum(cacheStats1)) // CacheStats(Some(1000), Some(8500L), Some(5600L), Some(500L), Some(13690L), Some(4013L))
println(sum(cacheStats2)) // CacheStats(None, None, None, None, None, None)
}
Example 3
Let's suppose we have several Options and we want to
combine them into an Option of a Tuple
Solution
val option1 = Option(1)
val option2 = Option(2)
val option3 = Option(3)
val option4 = Option(4)
val option5 = Option(5)
val option6 = Option(6)
val option7 = Option(7)
val option8 = Option(8)
val option9 = Option(9)
val option10 = Option(10)
println {
option1
.zip(option2)
.zip(option3)
.zip(option4)
.zip(option5)
.zip(option6)
.zip(option7)
.zip(option8)
.zip(option9)
.zip(option10)
.headOption
.map {
case (((((((((a, b), c), d), e), f), g), h), i), j) => (a, b, c, d, e, f, g, h, i, j)
}
}
// Some(Tuple10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
Example 4
Let's suppose we request some Person data from three
different servers, and we want to return the first successful
response, or a failure if all the requests fail
Solution
import com.twitter.util.{ Return, Throw, Try }
final case class Person(
firstName: String, lastName: String, country: String, state: String, age: Int
)
def getPerson(url: String): Try[Person] =
if (url.contains("1") || url.contains("2")) Throw(new Exception("Server error"))
else Return(Person("Ana", "Perez", "Bolivia", "La Paz", 30))
lazy val response1 = getPerson("http://server1.com/info")
lazy val response2 = getPerson("http://server2.com/info")
lazy val response3 = getPerson("http://server3.com/info")
val response: Try[Person] =
response1.rescue {
case _ =>
response2.rescue {
case _ => response3
}
}
println(response) // Return(Person(Ana,Perez,Bolivia,La Paz,30))
Solution
Example 5
Obtain the following information from a Binary Tree of Integers:
● The minimum value
● The maximum value
● The number of elements
● The number of elements greater than 5
● Does it contain the number 20?
● Does it contain negative numbers?
● Are all elements positive numbers?
● What’s the first element greater than 5?
● Is the tree empty?
● Is the tree non empty?
● What’s the sum of the elements?
● What’s the product of the elements?
● What’s the reversed tree?
Solution
sealed trait BinaryTree[+A]
object BinaryTree {
private final case class Leaf[A](value: A) extends BinaryTree[A]
private final case class Branch[A](left: BinaryTree[A], right: BinaryTree[A]) extends BinaryTree[A]
def leaf[A](value: A): BinaryTree[A] = BinaryTree.Leaf(value)
def branch[A](left: BinaryTree[A], right: BinaryTree[A]): BinaryTree[A] = BinaryTree.Branch(left, right)
}
Solution
sealed trait BinaryTree[+A] { self =>
import BinaryTree._
def contains[A1 >: A](a: A1): Boolean = self.count(_ == a) > 0
def count(f: A => Boolean): Int = self match {
case Leaf(a) => if (f(a)) 1 else 0
case Branch(l, r) => l.count(f) + r.count(f)
}
def exists(f: A => Boolean): Boolean = self.count(f) > 0
def find(f: A => Boolean): Option[A] = self match {
case Leaf(a) => Some(a).filter(f)
case Branch(l, r) => l.find(f).orElse(r.find(f))
}
def fold[A1 >: A](f: (A1, A1) => A1): A1 = self match {
case Leaf(a) => a
case Branch(left, right) => f(left.fold(f), right.fold(f))
}
def forall(f: A => Boolean): Boolean = self.count(f) == self.size
def isEmpty: Boolean = self.size == 0
...
}
Solution
sealed trait BinaryTree[+A] { self =>
import BinaryTree._
...
def map[B](f: A => B): BinaryTree[B] = self match {
case Leaf(a) => Leaf(f(a))
case Branch(left, right) => Branch(left.map(f), right.map(f))
}
def max[A1 >: A](implicit ordering: Ordering[A1]): A1 = self.maxBy(identity[A1])
def maxBy[B](f: A => B)(implicit ordering: Ordering[B]): A = self.fold(Ordering.by(f).max)
def min[A1 >: A](implicit ordering: Ordering[A1]): A1 = self.minBy(identity[A1])
def minBy[B](f: A => B)(implicit ordering: Ordering[B]): A = self.fold(Ordering.by(f).min)
def nonEmpty: Boolean = !self.isEmpty
def product[A1 >: A](implicit numeric: Numeric[A1]): A1 = self.fold(numeric.times)
def reverse: BinaryTree[A] = self match {
case Leaf(a) => Leaf(a)
case Branch(l, r) => Branch(r.reverse, l.reverse)
}
def sum[A1 >: A](implicit numeric: Numeric[A1]): A1 = self.fold(numeric.plus)
def size: Int = self.count(_ => true)
}
val tree = branch(
branch(
branch(
leaf(5),
leaf(10)
),
branch(
leaf(7),
leaf(8)
)
),
branch(
branch(
leaf(1),
leaf(11)
),
branch(
leaf(17),
leaf(21)
)
)
)
Solution
println(s"Minimum: ${tree.min}")
println(s"Maximum: ${tree.max}")
println(s"Number of elements: ${tree.size}")
println(s"Number of elements greater than 5: ${tree.count(_ > 5)}")
println(s"Does the tree contain the number 20?: ${tree.contains(20)}")
println(s"Does the tree contain negative numbers?: ${tree.exists(_ < 0)}")
println(s"Are all elements in the tree positive numbers?: ${tree.forall(_ > 0)}")
println(s"What's the first element greater than 5?: ${tree.find(_ > 5)}")
println(s"Is the tree empty?: ${tree.isEmpty}")
println(s"Is the tree non empty?: ${tree.nonEmpty}")
println(s"What's the sum of the elements (1st approach)?: ${tree.fold(_ + _)}")
println(s"What's the sum of the elements (2nd approach)?: ${tree.sum}")
println(s"What's the product of the elements?: ${tree.product}")
pprint.pprintln(s"Reversed tree: ${tree.reverse}")
Solution
Example 6
Obtain the following information from a Binary Tree of Person:
● Who’s the youngest person?
● Who’s the oldest person?
● What’s the average age?
● How many people are there per location?
Solution
sealed trait BinaryTree[+A] { self =>
import BinaryTree._
...
def groupBy[K](f: A => K): Map[K, List[A]] = self match {
case Leaf(a) => Map(f(a) -> List(a))
case Branch(l, r) =>
val leftMap = l.groupBy(f)
val rightMap = r.groupBy(f)
(leftMap.keySet ++ rightMap.keySet).map { key =>
(leftMap.get(key), rightMap.get(key)) match {
case (Some(as1), Some(as2)) => key -> (as1 ++ as2)
case (Some(as1), None) => key -> as1
case (None, Some(as2)) => key -> as2
case _ => throw new Error("Boom!")
}
}.toMap
}
...
}
Solution
val tree = branch(
branch(
branch(
leaf(Person("Adam", "Peterson", "USA", "California", 40)),
leaf(Person("Rachel", "Johns", "USA", "Los Angeles", 20))
),
branch(
leaf(Person("Jose", "Perez", "Bolivia", "La Paz", 35)),
leaf(Person("Monica", "Simpson", "UK", "London", 65))
)
),
branch(
branch(
leaf(Person("David", "Johnson", "USA", "California", 32)),
leaf(Person("Ana", "Sanchez", "Bolivia", "La Paz", 27))
),
branch(
leaf(Person("Laura", "Adams", "UK", "London", 54)),
leaf(Person("Roberto", "Mendes", "Brazil", "Minas Gerais", 24))
)
)
)
println(s"Youngest person: ${tree.minBy(_.age)}")
println(s"Oldest person: ${tree.maxBy(_.age)}")
println(s"What's the average age?: ${tree.map(_.age).sum / tree.size}")
println(
s"How many people are there per location?: ${tree.groupBy(person => (person.country, person.state)).mapValues(_.length)}"
)
Example 7
Process a Binary Tree of Strings, sending them to a server which just
echoes the received messages (encapsulated inside Future), and return
a Future of a Binary Tree containing the responses.
Solution
sealed trait BinaryTree[+A] { self =>
import BinaryTree._
...
def foreachFuture[B](f: A => Future[B]): Future[BinaryTree[B]] = self match {
case Leaf(a) => f(a).map(Leaf(_))
case Branch(l, r) => l.foreachFuture(f).zipWith(r.foreachFuture(f))(Branch(_, _))
}
...
}
Solution
def echo(message: String): Future[String] =
Future {
Thread.sleep(1000)
s"Echo: $message"
}
val messages =
branch(
branch(
leaf("message 1"),
leaf("message 2")
),
branch(
branch(
leaf("message 3"),
leaf("message 4")
),
branch(
leaf("message 5"),
leaf("message 6")
)
)
)
val responses = messages.foreachFuture(echo)
pprint.pprintln(Await.result(responses, 5.seconds))
/*
Branch(
Branch(Leaf("Echo: message 1"), Leaf("Echo: message 2")),
Branch(
Branch(Leaf("Echo: message 3"), Leaf("Echo: message 4")),
Branch(Leaf("Echo: message 5"), Leaf("Echo: message 6"))
)
)
*/
Solution
In conclusion
Enter
ZIO Prelude
What is ZIO Prelude?
Scala-first take on Functional Abstractions
ZIO Prelude gives us...
Data types that complement the Scala Standard Library:
● NonEmptyList
● NonEmptySet
● ZSet
● ZNonEmptySet
● Validation
● ZPure
ZIO Prelude gives us...
Newtypes that allow to increase type safety in domain
modeling, wrapping an existing type without adding any
runtime overhead.
ZIO Prelude gives us...
Typeclasses to describe similarities across different types, so
we can eliminate duplication/boilerplate:
● Business entities (Person, ShoppingCart, etc.)
● Effect-like structures (Try, Option, Future, Either, etc.)
● Collection-like structures (List, Tree, etc.)
Solving the
Boilerplate
Problem with
ZIO Prelude
Example 1
Let's suppose we have some cache stats data, organized
by date and application. This data comes from two
sources, and we need to combine them into one, by
summing counters when collisions exist.
Solution: Associative Typeclass
trait Associative[A] {
def combine(l: => A, r: => A): A
}
// Associativity law
(a1 <> (a2 <> a3)) <-> ((a1 <> a2) <> a3)
Solution: Associative Typeclass
ZIO Prelude has Associativeinstances for Scala Standard Types:
● Boolean
● Byte
● Char
● Short
● Int
● Long
● Float
● Double
● String
● Option
● Vector
● List
● Set
● Tuple
Solution: Associative Typeclass
And, of course, we can create instances of Associative for our
own types (or types on third party libraries)!
Solution: Associative Typeclass
final case class CacheStats(
entryCount: Option[Sum[Int]],
memorySize: Option[Sum[Long]],
hits: Option[Sum[Long]],
misses: Option[Sum[Long]],
loads: Option[Sum[Long]],
evictions: Option[Sum[Long]]
)
import zio.prelude._
object CacheStats {
def make(
entryCount: Option[Int],
memorySize: Option[Long],
hits: Option[Long],
misses: Option[Long],
loads: Option[Long],
evictions: Option[Long]
): CacheStats =
CacheStats(
entryCount.map(Sum(_)),
memorySize.map(Sum(_)),
hits.map(Sum(_)),
misses.map(Sum(_)),
loads.map(Sum(_)),
evictions.map(Sum(_))
)
implicit val associative = Associative.make[CacheStats] { (l, r) =>
CacheStats(
l.entryCount <> r.entryCount,
l.memorySize <> r.memorySize,
l.hits <> r.hits,
l.misses <> r.misses,
l.loads <> r.loads,
l.evictions <> r.evictions
)
}
}
Solution: Associative Typeclass
Solution: Associative Typeclass
● All typeclasses in ZIO Prelude include a set of laws that
instances must obey.
● We can use ZIO Test to check that laws of a typeclass are
fulfilled by a given instance, using Property Based Testing.
Solution: Associative Typeclass
object CacheStatsSpec extends DefaultRunnableSpec {
def spec = suite("CacheStatsSpec")(
suite("CacheStats")(
testM("associative")(checkAllLaws(Associative)(cacheStatsGen))
)
)
def cacheStatsGen[R <: Random with Sized]: Gen[R, CacheStats] = {
val intGen = Gen.oneOf(Gen.none, Gen.anyInt.map(Sum(_)).map(Some(_)))
val longGen = Gen.oneOf(Gen.none, Gen.anyLong.map(Sum(_)).map(Some(_)))
for {
entryCount <- intGen
memorySize <- longGen
hits <- longGen
misses <- longGen
loads <- longGen
evictions <- longGen
} yield CacheStats(entryCount, memorySize, hits, misses, loads, evictions)
}
}
Solution: Associative Typeclass
import zio.prelude._
pprint.pprintln(
Associative[Map[LocalDate, Map[String, CacheStats]]].combine(
stats1, stats2
)
)
/*
Map(
2020-01-18 -> Map(
"App X" -> CacheStats(Some(1000), Some(10000L), Some(4000L), Some(1000L),
Some(10000L), Some(5000L)),
"App Y" -> CacheStats(Some(800), Some(3500L), Some(6200L), None,
Some(15780L), Some(3026L)),
"App Z" -> CacheStats(None, Some(678L), Some(500L), None, Some(1600L), None)
),
2020-01-19 -> Map(
"App A" -> CacheStats(None, None, Some(8196L), None, Some(10836L), None),
"App B" -> CacheStats(Some(3134), Some(2854L), Some(8196L), Some(2000L),
Some(10836L), Some(6000L)),
"App C" -> CacheStats(None, None, Some(1000L), Some(467L), Some(1600L),
None)
),
2020-03-05 -> Map(
"App A" -> CacheStats(Some(8756), Some(1000L), Some(6420L), Some(2000L),
None, None),
"App Y" -> CacheStats(None, Some(1345L), Some(18064L), Some(246L), None,
None)
),
2020-04-10 -> Map("App X" -> CacheStats(Some(1879), None, Some(864L), None,
Some(5082L), None))
)
*/
val stats1: Map[LocalDate, Map[String, CacheStats]] = Map(
LocalDate.of(2020, 1, 18) -> Map(
"App X" -> CacheStats.make(Some(1000), Some(5000), Some(2000), Some(500), Some(5000), Some(2500)),
"App Y" -> CacheStats.make(None, Some(3500), Some(3100), None, Some(7890), Some(1513)),
"App Z" -> CacheStats.make(None, None, Some(500), None, Some(800), None)
),
LocalDate.of(2020, 1, 19) -> Map(
"App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None),
"App B" -> CacheStats.make(Some(1567), Some(2854), Some(4098), Some(1000), Some(5418), Some(3000)),
"App C" -> CacheStats.make(None, None, Some(500), None, Some(800), None)
),
LocalDate.of(2020, 3, 5) -> Map(
"App A" -> CacheStats.make(Some(4378), Some(1000), Some(3210), Some(1000), None, None),
"App Y" -> CacheStats.make(None, None, Some(9032), Some(123), None, None)
),
LocalDate.of(2020, 4, 10) -> Map(
"App X" -> CacheStats.make(Some(1879), None, Some(432), None, Some(2541), None)
)
)
val stats2: Map[LocalDate, Map[String, CacheStats]] = Map(
LocalDate.of(2020, 1, 18) -> Map(
"App X" -> CacheStats.make(None, Some(5000), Some(2000), Some(500), Some(5000), Some(2500)),
"App Y" -> CacheStats.make(Some(800), None, Some(3100), None, Some(7890), Some(1513)),
"App Z" -> CacheStats.make(None, Some(678), None, None, Some(800), None)
),
LocalDate.of(2020, 1, 19) -> Map(
"App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None),
"App B" -> CacheStats.make(Some(1567), None, Some(4098), Some(1000), Some(5418), Some(3000)),
"App C" -> CacheStats.make(None, None, Some(500), Some(467), Some(800), None)
),
LocalDate.of(2020, 3, 5) -> Map(
"App A" -> CacheStats.make(Some(4378), None, Some(3210), Some(1000), None, None),
"App Y" -> CacheStats.make(None, Some(1345), Some(9032), Some(123), None, None)
),
LocalDate.of(2020, 4, 10) -> Map(
"App X" -> CacheStats.make(None, None, Some(432), None, Some(2541), None)
)
)
Solution: Associative Typeclass
import zio.prelude._
pprint.pprintln(stats1 <> stats2)
/*
Map(
2020-01-18 -> Map(
"App X" -> CacheStats(Some(1000), Some(10000L), Some(4000L),
Some(1000L), Some(10000L), Some(5000L)),
"App Y" -> CacheStats(Some(800), Some(3500L), Some(6200L), None,
Some(15780L), Some(3026L)),
"App Z" -> CacheStats(None, Some(678L), Some(500L), None, Some(1600L),
None)
),
2020-01-19 -> Map(
"App A" -> CacheStats(None, None, Some(8196L), None, Some(10836L),
None),
"App B" -> CacheStats(Some(3134), Some(2854L), Some(8196L),
Some(2000L), Some(10836L), Some(6000L)),
"App C" -> CacheStats(None, None, Some(1000L), Some(467L), Some(1600L),
None)
),
2020-03-05 -> Map(
"App A" -> CacheStats(Some(8756), Some(1000L), Some(6420L),
Some(2000L), None, None),
"App Y" -> CacheStats(None, Some(1345L), Some(18064L), Some(246L),
None, None)
),
2020-04-10 -> Map("App X" -> CacheStats(Some(1879), None, Some(864L),
None, Some(5082L), None))
)
*/
val stats1: Map[LocalDate, Map[String, CacheStats]] = Map(
LocalDate.of(2020, 1, 18) -> Map(
"App X" -> CacheStats.make(Some(1000), Some(5000), Some(2000), Some(500), Some(5000), Some(2500)),
"App Y" -> CacheStats.make(None, Some(3500), Some(3100), None, Some(7890), Some(1513)),
"App Z" -> CacheStats.make(None, None, Some(500), None, Some(800), None)
),
LocalDate.of(2020, 1, 19) -> Map(
"App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None),
"App B" -> CacheStats.make(Some(1567), Some(2854), Some(4098), Some(1000), Some(5418), Some(3000)),
"App C" -> CacheStats.make(None, None, Some(500), None, Some(800), None)
),
LocalDate.of(2020, 3, 5) -> Map(
"App A" -> CacheStats.make(Some(4378), Some(1000), Some(3210), Some(1000), None, None),
"App Y" -> CacheStats.make(None, None, Some(9032), Some(123), None, None)
),
LocalDate.of(2020, 4, 10) -> Map(
"App X" -> CacheStats.make(Some(1879), None, Some(432), None, Some(2541), None)
)
)
val stats2: Map[LocalDate, Map[String, CacheStats]] = Map(
LocalDate.of(2020, 1, 18) -> Map(
"App X" -> CacheStats.make(None, Some(5000), Some(2000), Some(500), Some(5000), Some(2500)),
"App Y" -> CacheStats.make(Some(800), None, Some(3100), None, Some(7890), Some(1513)),
"App Z" -> CacheStats.make(None, Some(678), None, None, Some(800), None)
),
LocalDate.of(2020, 1, 19) -> Map(
"App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None),
"App B" -> CacheStats.make(Some(1567), None, Some(4098), Some(1000), Some(5418), Some(3000)),
"App C" -> CacheStats.make(None, None, Some(500), Some(467), Some(800), None)
),
LocalDate.of(2020, 3, 5) -> Map(
"App A" -> CacheStats.make(Some(4378), None, Some(3210), Some(1000), None, None),
"App Y" -> CacheStats.make(None, Some(1345), Some(9032), Some(123), None, None)
),
LocalDate.of(2020, 4, 10) -> Map(
"App X" -> CacheStats.make(None, None, Some(432), None, Some(2541), None)
)
)
Solution: Associative Typeclass
Example 2
Let's suppose we have a List of cache stats data, and we
want to sum all stats.
Solution: Identity Typeclass
trait Identity[A] extends Associative[A] {
def identity: A
}
// Associativity law
(a1 <> (a2 <> a3)) <-> ((a1 <> a2) <> a3)
// Left Identity law
(identity <> a) <-> a
// Right Identity law
(a <> identity) <-> a
Solution: Identity Typeclass
final case class CacheStats(
entryCount: Option[Sum[Int]],
memorySize: Option[Sum[Long]],
hits: Option[Sum[Long]],
misses: Option[Sum[Long]],
loads: Option[Sum[Long]],
evictions: Option[Sum[Long]]
)
import zio.prelude._
object CacheStats {
def make(
entryCount: Option[Int],
memorySize: Option[Long],
hits: Option[Long],
misses: Option[Long],
loads: Option[Long],
evictions: Option[Long]
): CacheStats =
CacheStats(
entryCount.map(Sum(_)),
memorySize.map(Sum(_)),
hits.map(Sum(_)),
misses.map(Sum(_)),
loads.map(Sum(_)),
evictions.map(Sum(_))
)
implicit val identity = Identity.make[CacheStats](
CacheStats.make(None, None, None, None, None, None),
(l, r) =>
CacheStats(
l.entryCount <> r.entryCount,
l.memorySize <> r.memorySize,
l.hits <> r.hits,
l.misses <> r.misses,
l.loads <> r.loads,
l.evictions <> r.evictions
)
)
}
Solution: Identity Typeclass
def sum(cacheStats: List[CacheStats]): CacheStats =
cacheStats.foldRight(Identity[CacheStats].identity)(_ <> _)
def main(args: Array[String]): Unit = {
val cacheStats1 = List(
CacheStats.make(Some(1000), Some(5000), Some(2000), Some(500), Some(5000), Some(2500)),
CacheStats.make(None, Some(3500), Some(3100), None, Some(7890), Some(1513)),
CacheStats.make(None, None, Some(500), None, Some(800), None)
)
val cacheStats2 = List.empty[CacheStats]
println(sum(cacheStats1)) // CacheStats(Some(1000), Some(8500L), Some(5600L), Some(500L), Some(13690L), Some(4013L))
println(sum(cacheStats2)) // CacheStats(None, None, None, None, None, None)
}
Solution: Identity Typeclass
Example 3
Let's suppose we have several Options and we want to
combine them into an Option of a Tuple
Solution: AssociativeBoth Typeclass
trait AssociativeBoth[F[_]] {
def both[A, B](fa: => F[A], fb: => F[B]): F[(A, B)]
}
// Associativity law
both(fa, both(fb, fc)) ~ both(both(fa, fb), fc)
Solution: AssociativeBoth Typeclass
ZIO Prelude has AssociativeBothinstances for Scala Standard Types:
● Either
● Future
● List
● Option
● Try
● Vector
Solution: AssociativeBoth Typeclass
import zio.prelude._
def main(args: Array[String]): Unit = {
val option1 = Option(1)
val option2 = Option(2)
val option3 = Option(3)
val option4 = Option(4)
val option5 = Option(5)
val option6 = Option(6)
val option7 = Option(7)
val option8 = Option(8)
val option9 = Option(9)
val option10 = Option(10)
println {
AssociativeBoth.tupleN(option1, option2, option3, option4, option5, option6, option7, option8, option9, option10)
}
// Some(Tuple10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
}
Solution: AssociativeBoth Typeclass
Example 4
Let's suppose we request some Person data from three
different servers, and we want to return the first successful
response, or a failure if all the requests fail
Solution: AssociativeEither Typeclass
trait AssociativeEither[F[_]] {
def either[A, B](fa: => F[A], fb: => F[B]): F[Either[A, B]]
}
// Associativity law
either(fa, either(fb, fc)) ~ either(either(fa, fb), fc)
Solution: AssociativeEither Typeclass
import com.twitter.util.{ Return, Throw, Try }
import zio.prelude._
implicit val TryAssociativeEither = new AssociativeEither[Try] {
def either[A, B](fa: => Try[A], fb: => Try[B]): Try[Either[A, B]] =
fa.map(Left(_)) rescue {
case _ => fb.map(Right(_))
}
}
Solution: AssociativeEither Typeclass
import com.twitter.util.{ Return, Throw, Try }
import zio.prelude._
final case class Person(firstName: String, lastName: String, country: String, state: String, age: Int)
def getPerson(url: String): Try[Person] =
if (url.contains("1") || url.contains("2")) Throw(new Exception("Server error"))
else Return(Person("Ana", "Perez", "Bolivia", "La Paz", 30))
def main(args: Array[String]): Unit = {
lazy val response1 = getPerson("http://server1.com/info")
lazy val response2 = getPerson("http://server2.com/info")
lazy val response3 = getPerson("http://server3.com/info")
val response: Try[Person] = response1 orElse response2 orElse response3
println(response)
// Return(Person("Ana", "Perez", "Bolivia", "La Paz", 30))
}
Solution: AssociativeEither Typeclass
Example 5
Obtain the following information from a Binary Tree of Integers:
● The minimum value
● The maximum value
● The number of elements
● The number of elements greater than 5
● Does it contain the number 20?
● Does it contain negative numbers?
● Are all elements positive numbers?
● What’s the first element greater than 5?
● Is the tree empty?
● Is the tree non empty?
● What’s the sum of the elements?
● What’s the product of the elements?
● What’s the reversed tree?
Solution: Traversable Typeclass
trait Traversable[F[+_]] extends Covariant[F] {
def foreach[G[+_]: IdentityBoth: Covariant, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
// A lot of methods for free!
def contains[A, A1 >: A](fa: F[A])(a: A1)(implicit A: Equal[A1]): Boolean
def count[A](fa: F[A])(f: A => Boolean): Int
def exists[A](fa: F[A])(f: A => Boolean): Boolean
def find[A](fa: F[A])(f: A => Boolean): Option[A]
def flip[G[+_]: IdentityBoth: Covariant, A](fa: F[G[A]]): G[F[A]]
def fold[A: Identity](fa: F[A]): A
def foldLeft[S, A](fa: F[A])(s: S)(f: (S, A) => S): S
def foldMap[A, B: Identity](fa: F[A])(f: A => B): B
def foldRight[S, A](fa: F[A])(s: S)(f: (A, S) => S): S
def forall[A](fa: F[A])(f: A => Boolean): Boolean
def foreach_[G[+_]: IdentityBoth: Covariant, A](fa: F[A])(f: A => G[Any]): G[Unit]
def isEmpty[A](fa: F[A]): Boolean
def map[A, B](f: A => B): F[A] => F[B]
def mapAccum[S, A, B](fa: F[A])(s: S)(f: (S, A) => (S, B)): (S, F[B])
def maxOption[A: Ord](fa: F[A]): Option[A]
def maxByOption[A, B: Ord](fa: F[A])(f: A => B): Option[A]
def minOption[A: Ord](fa: F[A]): Option[A]
def minByOption[A, B: Ord](fa: F[A])(f: A => B): Option[A]
def nonEmpty[A](fa: F[A]): Boolean
def product[A](fa: F[A])(implicit ev: Identity[Prod[A]]): A
def reduceMapOption[A, B: Associative](fa: F[A])(f: A => B): Option[B]
def reduceOption[A](fa: F[A])(f: (A, A) => A): Option[A]
def reverse[A](fa: F[A]): F[A]
def size[A](fa: F[A]): Int
def sum[A](fa: F[A])(implicit ev: Identity[Sum[A]]): A
def toChunk[A](fa: F[A]): Chunk[A]
def toList[A](fa: F[A]): List[A]
def zipWithIndex[A](fa: F[A]): F[(A, Int)]
}
Solution: Traversable Typeclass
sealed trait BinaryTree[+A] { self =>
import BinaryTree._
def map[B](f: A => B): BinaryTree[B] = self match {
case Leaf(a) => Leaf(f(a))
case Branch(left, right) => Branch(left.map(f), right.map(f))
}
}
object BinaryTree {
private final case class Leaf[A](value: A) extends BinaryTree[A]
private final case class Branch[A](left: BinaryTree[A], right: BinaryTree[A]) extends BinaryTree[A]
def leaf[A](value: A): BinaryTree[A] = BinaryTree.Leaf(value)
def branch[A](left: BinaryTree[A], right: BinaryTree[A]): BinaryTree[A] = BinaryTree.Branch(left, right)
implicit val traversable = new Traversable[BinaryTree] {
def foreach[G[+ _]: IdentityBoth: Covariant, A, B](fa: BinaryTree[A])(f: A => G[B]): G[BinaryTree[B]] = fa match {
case Leaf(a) => f(a).map(Leaf(_))
case Branch(l, r) => foreach(l)(f).zipWith(foreach(r)(f))(Branch(_, _))
}
override def map[A, B](f: A => B): BinaryTree[A] => BinaryTree[B] = _.map(f)
}
}
val tree = branch(
branch(
branch(
leaf(5),
leaf(10)
),
branch(
leaf(7),
leaf(8)
)
),
branch(
branch(
leaf(1),
leaf(11)
),
branch(
leaf(17),
leaf(21)
)
)
)
Solution: Traversable Typeclass
println(s"Minimum: ${tree.minOption}")
println(s"Maximum: ${tree.maxOption}")
println(s"Number of elements: ${tree.size}")
println(s"Number of elements greater than 5: ${tree.count(_ > 5)}")
println(s"Does the tree contain the number 20?: ${tree.contains(20)}")
println(s"Does the tree contain negative numbers?: ${tree.exists(_ < 0)}")
println(s"Are all elements in the tree positive numbers?: ${tree.forall(_ > 0)}")
println(s"What's the first element greater than 5?: ${tree.find(_ > 5)}")
println(s"Is the tree empty?: ${tree.isEmpty}")
println(s"Is the tree non empty?: ${tree.nonEmpty}")
println(s"What's the sum of the elements: ${tree.sum}")
println(s"What's the product of the elements?: ${tree.product}")
pprint.pprintln(s"Reversed tree: ${tree.reverse}")
Solution: Traversable Typeclass
Example 6
Obtain the following information from a Binary Tree of Person:
● Who’s the youngest person?
● Who’s the oldest person?
● What’s the average age?
● How many people are there per location?
Solution: Traversable Typeclass
val tree = branch(
branch(
branch(
leaf(Person("Adam", "Peterson", "USA", "California", 40)),
leaf(Person("Rachel", "Johns", "USA", "Los Angeles", 20))
),
branch(
leaf(Person("Jose", "Perez", "Bolivia", "La Paz", 35)),
leaf(Person("Monica", "Simpson", "UK", "London", 65))
)
),
branch(
branch(
leaf(Person("David", "Johnson", "USA", "California", 32)),
leaf(Person("Ana", "Sanchez", "Bolivia", "La Paz", 27))
),
branch(
leaf(Person("Laura", "Adams", "UK", "London", 54)),
leaf(Person("Roberto", "Mendes", "Brazil", "Minas Gerais", 24))
)
)
)
println(s"Youngest person: ${tree.minByOption(_.age)}")
println(s"Oldest person: ${tree.maxByOption(_.age)}")
println(s"What's the average age?: ${tree.map(_.age).sum / tree.size}")
println(
s"How many people are there per location?: ${Traversable[BinaryTree].groupBy(tree)(person => (person.country, person.state)).mapValues(_.length)}"
)
Solution: NonEmptyTraversable Typeclass
trait NonEmptyTraversable[F[+_]] extends Traversable[F] {
def foreach1[G[+_]: AssociativeBoth: Covariant, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
def flip1[G[+_]: AssociativeBoth: Covariant, A](fa: F[G[A]]): G[F[A]]
override def foreach[G[+_]: IdentityBoth: Covariant, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
def foreach1_[G[+_]: AssociativeBoth: Covariant, A](fa: F[A])(f: A => G[Any]): G[Unit]
def max[A: Ord](fa: F[A]): A
def maxBy[A, B: Ord](fa: F[A])(f: A => B): A
def min[A: Ord](fa: F[A]): A
def minBy[A, B: Ord](fa: F[A])(f: A => B): A
def reduce[A](fa: F[A])(f: (A, A) => A): A
def reduce1[A: Associative](fa: F[A]): A
def reduceMap[A, B: Associative](fa: F[A])(f: A => B): B
def reduceMapLeft[A, B](fa: F[A])(map: A => B)(reduce: (B, A) => B): B
def reduceMapRight[A, B](fa: F[A])(map: A => B)(reduce: (A, B) => B): B
def toNonEmptyChunk[A](fa: F[A]): NonEmptyChunk[A]
def toNonEmptyList[A](fa: F[A]): NonEmptyList[A]
}
Solution: NonEmptyTraversable Typeclass
sealed trait BinaryTree[+A] { self =>
import BinaryTree._
def map[B](f: A => B): BinaryTree[B] = self match {
case Leaf(a) => Leaf(f(a))
case Branch(left, right) => Branch(left.map(f), right.map(f))
}
}
object BinaryTree {
private final case class Leaf[A](value: A) extends BinaryTree[A]
private final case class Branch[A](left: BinaryTree[A], right: BinaryTree[A]) extends BinaryTree[A]
def leaf[A](value: A): BinaryTree[A] = BinaryTree.Leaf(value)
def branch[A](left: BinaryTree[A], right: BinaryTree[A]): BinaryTree[A] = BinaryTree.Branch(left, right)
implicit val nonEmptyTraversable = new NonEmptyTraversable[BinaryTree] {
def foreach1[G[+ _]: AssociativeBoth: Covariant, A, B](fa: BinaryTree[A])(f: A => G[B]): G[BinaryTree[B]] = fa match {
case Leaf(a) => f(a).map(Leaf(_))
case Branch(l, r) => foreach1(l)(f).zipWith(foreach1(r)(f))(Branch(_, _))
}
}
}
Solution: NonEmptyTraversable Typeclass
val tree = branch(
branch(
branch(
leaf(Person("Adam", "Peterson", "USA", "California", 40)),
leaf(Person("Rachel", "Johns", "USA", "Los Angeles", 20))
),
branch(
leaf(Person("Jose", "Perez", "Bolivia", "La Paz", 35)),
leaf(Person("Monica", "Simpson", "UK", "London", 65))
)
),
branch(
branch(
leaf(Person("David", "Johnson", "USA", "California", 32)),
leaf(Person("Ana", "Sanchez", "Bolivia", "La Paz", 27))
),
branch(
leaf(Person("Laura", "Adams", "UK", "London", 54)),
leaf(Person("Roberto", "Mendes", "Brazil", "Minas Gerais", 24))
)
)
)
println(s"Youngest person: ${NonEmptyTraversable[BinaryTree].minBy(tree)(_.age)}")
println(s"Oldest person: ${NonEmptyTraversable[BinaryTree].maxBy(tree)(_.age)}")
Solution: NonEmptyTraversable Typeclass
Example 7
Process a Binary Tree of Strings, sending them to a server which just
echoes the received messages (encapsulated inside Future), and return
a Future of a Binary Tree containing the responses.
Solution: Traversable Typeclass
def echo(message: String): Future[String] =
Future {
Thread.sleep(1000)
s"Echo: $message"
}
val messages =
branch(
branch(
leaf("message 1"),
leaf("message 2")
),
branch(
branch(
leaf("message 3"),
leaf("message 4")
),
branch(
leaf("message 5"),
leaf("message 6")
)
)
)
val responses = messages.foreach(echo)
pprint.pprintln(Await.result(responses, 5.seconds))
/*
Branch(
Branch(Leaf("Echo: message 1"), Leaf("Echo: message 2")),
Branch(
Branch(Leaf("Echo: message 3"), Leaf("Echo: message 4")),
Branch(Leaf("Echo: message 5"), Leaf("Echo: message 6"))
)
)
*/
In conclusion
Summary
● There's a lot of boilerplate in our applications
● Abstraction is the key to eliminating boilerplate
● Functional code uses typeclasses for abstraction
Summary
ZIO Prelude has typeclasses that let you eliminate boilerplate across:
● Business entities:
○ Associative/Identity
● Effect-like structures:
○ AssociativeBoth/IdentityBoth
○ AssociativeEither/IdentityEither
● Collection-like structures:
○ Traversable/NonEmptyTraversable
Special thanks
● To Functional Scala organizers for hosting this presentation
● To John De Goes for guidance and support
Thank You!
Where to learn more
● ZIO Prelude on Github: https://github.com/zio/zio-prelude/
● SF Scala: Reimagining Functional Type Classes, talk by
John De Goes and Adam Fraser
● Functional World: Exploring ZIO Prelude - The game
changer for typeclasses in Scala, talk by Jorge Vásquez
@jorvasquez2301
jorge.vasquez@scalac.io
jorge-vasquez-2301
Contact me
The Terror-Free Guide to Introducing Functional Scala at Work

The Terror-Free Guide to Introducing Functional Scala at Work

  • 1.
    The Terror-Free Guideto Introducing Functional Scala at Work (without dying in the process)
  • 2.
  • 4.
    We are hiringScala Developers! https://scalac.io/careers/
  • 5.
    Agenda ● Motivation ● ZIOPrelude Overview ● How ZIO Prelude can help us
  • 6.
  • 7.
    Example 1 Let's supposewe have some cache stats data, organized by date and application. This data comes from two sources, and we need to combine them into one, by summing counters when collisions exist.
  • 8.
  • 9.
    Solution 1 final caseclass CacheStats( entryCount: Option[Int], memorySize: Option[Long], hits: Option[Long], misses: Option[Long], loads: Option[Long], evictions: Option[Long] ) object CacheStats { def make( entryCount: Option[Int], memorySize: Option[Long], hits: Option[Long], misses: Option[Long], loads: Option[Long], evictions: Option[Long] ): CacheStats = CacheStats(entryCount, memorySize, hits, misses, loads, evictions) }
  • 10.
    Solution 1 pprint.pprintln(stats1 ++stats2) /* Map( 2020-01-18 -> Map( "App X" -> CacheStats(None, Some(5000L), Some(2000L), Some(500L), Some(5000L), Some(2500L)), "App Y" -> CacheStats(Some(800), None, Some(3100L), None, Some(7890L), Some(1513L)), "App Z" -> CacheStats(None, Some(678L), None, None, Some(800L), None) ), 2020-01-19 -> Map( "App A" -> CacheStats(None, None, Some(4098L), None, Some(5418L), None), "App B" -> CacheStats(Some(1567), None, Some(4098L), Some(1000L), Some(5418L), Some(3000L)), "App C" -> CacheStats(None, None, Some(500L), Some(467L), Some(800L), None) ), 2020-03-05 -> Map( "App A" -> CacheStats(Some(4378), None, Some(3210L), Some(1000L), None, None), "App Y" -> CacheStats(None, Some(1345L), Some(9032L), Some(123L), None, None) ), 2020-04-10 -> Map("App X" -> CacheStats(None, None, Some(432L), None, Some(2541L), None)) ) */ val stats1: Map[LocalDate, Map[String, CacheStats]] = Map( LocalDate.of(2020, 1, 18) -> Map( "App X" -> CacheStats.make(Some(1000), Some(5000), Some(2000), Some(500), Some(5000), Some(2500)), "App Y" -> CacheStats.make(None, Some(3500), Some(3100), None, Some(7890), Some(1513)), "App Z" -> CacheStats.make(None, None, Some(500), None, Some(800), None) ), LocalDate.of(2020, 1, 19) -> Map( "App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None), "App B" -> CacheStats.make(Some(1567), Some(2854), Some(4098), Some(1000), Some(5418), Some(3000)), "App C" -> CacheStats.make(None, None, Some(500), None, Some(800), None) ), LocalDate.of(2020, 3, 5) -> Map( "App A" -> CacheStats.make(Some(4378), Some(1000), Some(3210), Some(1000), None, None), "App Y" -> CacheStats.make(None, None, Some(9032), Some(123), None, None) ), LocalDate.of(2020, 4, 10) -> Map( "App X" -> CacheStats.make(Some(1879), None, Some(432), None, Some(2541), None) ) ) val stats2: Map[LocalDate, Map[String, CacheStats]] = Map( LocalDate.of(2020, 1, 18) -> Map( "App X" -> CacheStats.make(None, Some(5000), Some(2000), Some(500), Some(5000), Some(2500)), "App Y" -> CacheStats.make(Some(800), None, Some(3100), None, Some(7890), Some(1513)), "App Z" -> CacheStats.make(None, Some(678), None, None, Some(800), None) ), LocalDate.of(2020, 1, 19) -> Map( "App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None), "App B" -> CacheStats.make(Some(1567), None, Some(4098), Some(1000), Some(5418), Some(3000)), "App C" -> CacheStats.make(None, None, Some(500), Some(467), Some(800), None) ), LocalDate.of(2020, 3, 5) -> Map( "App A" -> CacheStats.make(Some(4378), None, Some(3210), Some(1000), None, None), "App Y" -> CacheStats.make(None, Some(1345), Some(9032), Some(123), None, None) ), LocalDate.of(2020, 4, 10) -> Map( "App X" -> CacheStats.make(None, None, Some(432), None, Some(2541), None) ) )
  • 11.
  • 12.
  • 13.
    Solution 2 final caseclass CacheStats( entryCount: Option[Int], memorySize: Option[Long], hits: Option[Long], misses: Option[Long], loads: Option[Long], evictions: Option[Long] ) { self => def combine(that: CacheStats): CacheStats = { def combineCounters[A: Numeric](left: Option[A], right: Option[A]): Option[A] = (left, right) match { case (Some(l), Some(r)) => Some(implicitly[Numeric[A]].plus(l, r)) case (Some(l), None) => Some(l) case (None, Some(r)) => Some(r) case (None, None) => None } CacheStats( combineCounters(self.entryCount, that.entryCount), combineCounters(self.memorySize, that.memorySize), combineCounters(self.hits, that.hits), combineCounters(self.misses, that.misses), combineCounters(self.loads, that.loads), combineCounters(self.evictions, that.evictions) ) } }
  • 14.
    Solution 2 def combine( left:Map[LocalDate, Map[String, CacheStats]], right: Map[LocalDate, Map[String, CacheStats]] ): Map[LocalDate, Map[String, CacheStats]] = { (left.keySet ++ right.keySet).map { date => val newStatsByApp = (left.get(date), right.get(date)) match { case (Some(v1), None) => v1 case (None, Some(v2)) => v2 case (Some(v1), Some(v2)) => (v1.keySet ++ v2.keySet).map { location => val newStats = (v1.get(location), v2.get(location)) match { case (Some(s1), None) => s1 case (None, Some(s2)) => s2 case (Some(s1), Some(s2)) => s1 combine s2 case (None, None) => throw new Error("Unexpected scenario") } location -> newStats }.toMap case (None, None) => throw new Error("Unexpected scenario") } date -> newStatsByApp } }.toMap
  • 15.
    Solution 2 pprint.pprintln(combine(stats1, stats2)) /* Map( 2020-01-18-> Map( "App X" -> CacheStats(Some(1000), Some(10000L), Some(4000L), Some(1000L), Some(10000L), Some(5000L)), "App Y" -> CacheStats(Some(800), Some(3500L), Some(6200L), None, Some(15780L), Some(3026L)), "App Z" -> CacheStats(None, Some(678L), Some(500L), None, Some(1600L), None) ), 2020-01-19 -> Map( "App A" -> CacheStats(None, None, Some(8196L), None, Some(10836L), None), "App B" -> CacheStats(Some(3134), Some(2854L), Some(8196L), Some(2000L), Some(10836L), Some(6000L)), "App C" -> CacheStats(None, None, Some(1000L), Some(467L), Some(1600L), None) ), 2020-03-05 -> Map( "App A" -> CacheStats(Some(8756), Some(1000L), Some(6420L), Some(2000L), None, None), "App Y" -> CacheStats(None, Some(1345L), Some(18064L), Some(246L), None, None) ), 2020-04-10 -> Map("App X" -> CacheStats(Some(1879), None, Some(864L), None, Some(5082L), None)) ) */ val stats1: Map[LocalDate, Map[String, CacheStats]] = Map( LocalDate.of(2020, 1, 18) -> Map( "App X" -> CacheStats.make(Some(1000), Some(5000), Some(2000), Some(500), Some(5000), Some(2500)), "App Y" -> CacheStats.make(None, Some(3500), Some(3100), None, Some(7890), Some(1513)), "App Z" -> CacheStats.make(None, None, Some(500), None, Some(800), None) ), LocalDate.of(2020, 1, 19) -> Map( "App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None), "App B" -> CacheStats.make(Some(1567), Some(2854), Some(4098), Some(1000), Some(5418), Some(3000)), "App C" -> CacheStats.make(None, None, Some(500), None, Some(800), None) ), LocalDate.of(2020, 3, 5) -> Map( "App A" -> CacheStats.make(Some(4378), Some(1000), Some(3210), Some(1000), None, None), "App Y" -> CacheStats.make(None, None, Some(9032), Some(123), None, None) ), LocalDate.of(2020, 4, 10) -> Map( "App X" -> CacheStats.make(Some(1879), None, Some(432), None, Some(2541), None) ) ) val stats2: Map[LocalDate, Map[String, CacheStats]] = Map( LocalDate.of(2020, 1, 18) -> Map( "App X" -> CacheStats.make(None, Some(5000), Some(2000), Some(500), Some(5000), Some(2500)), "App Y" -> CacheStats.make(Some(800), None, Some(3100), None, Some(7890), Some(1513)), "App Z" -> CacheStats.make(None, Some(678), None, None, Some(800), None) ), LocalDate.of(2020, 1, 19) -> Map( "App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None), "App B" -> CacheStats.make(Some(1567), None, Some(4098), Some(1000), Some(5418), Some(3000)), "App C" -> CacheStats.make(None, None, Some(500), Some(467), Some(800), None) ), LocalDate.of(2020, 3, 5) -> Map( "App A" -> CacheStats.make(Some(4378), None, Some(3210), Some(1000), None, None), "App Y" -> CacheStats.make(None, Some(1345), Some(9032), Some(123), None, None) ), LocalDate.of(2020, 4, 10) -> Map( "App X" -> CacheStats.make(None, None, Some(432), None, Some(2541), None) ) )
  • 16.
    Example 2 Let's supposewe have a List of cache stats data, and we want to sum all stats.
  • 17.
    Solution final case classCacheStats( entryCount: Option[Int], memorySize: Option[Long], hits: Option[Long], misses: Option[Long], loads: Option[Long], evictions: Option[Long] ) { self => def combine(that: CacheStats): CacheStats = { def combineCounters[A: Numeric](left: Option[A], right: Option[A]): Option[A] = (left, right) match { case (Some(l), Some(r)) => Some(implicitly[Numeric[A]].plus(l, r)) case (Some(l), None) => Some(l) case (None, Some(r)) => Some(r) case (None, None) => None } CacheStats( combineCounters(self.entryCount, that.entryCount), combineCounters(self.memorySize, that.memorySize), combineCounters(self.hits, that.hits), combineCounters(self.misses, that.misses), combineCounters(self.loads, that.loads), combineCounters(self.evictions, that.evictions) ) } }
  • 18.
    Solution def sum(cacheStats: List[CacheStats]):CacheStats = cacheStats.foldRight(CacheStats.make(None, None, None, None, None, None))(_ combine _) def main(args: Array[String]): Unit = { val cacheStats1 = List( CacheStats.make(Some(1000), Some(5000), Some(2000), Some(500), Some(5000), Some(2500)), CacheStats.make(None, Some(3500), Some(3100), None, Some(7890), Some(1513)), CacheStats.make(None, None, Some(500), None, Some(800), None) ) val cacheStats2 = List.empty[CacheStats] println(sum(cacheStats1)) // CacheStats(Some(1000), Some(8500L), Some(5600L), Some(500L), Some(13690L), Some(4013L)) println(sum(cacheStats2)) // CacheStats(None, None, None, None, None, None) }
  • 19.
    Example 3 Let's supposewe have several Options and we want to combine them into an Option of a Tuple
  • 20.
    Solution val option1 =Option(1) val option2 = Option(2) val option3 = Option(3) val option4 = Option(4) val option5 = Option(5) val option6 = Option(6) val option7 = Option(7) val option8 = Option(8) val option9 = Option(9) val option10 = Option(10) println { option1 .zip(option2) .zip(option3) .zip(option4) .zip(option5) .zip(option6) .zip(option7) .zip(option8) .zip(option9) .zip(option10) .headOption .map { case (((((((((a, b), c), d), e), f), g), h), i), j) => (a, b, c, d, e, f, g, h, i, j) } } // Some(Tuple10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
  • 21.
    Example 4 Let's supposewe request some Person data from three different servers, and we want to return the first successful response, or a failure if all the requests fail
  • 22.
    Solution import com.twitter.util.{ Return,Throw, Try } final case class Person( firstName: String, lastName: String, country: String, state: String, age: Int ) def getPerson(url: String): Try[Person] = if (url.contains("1") || url.contains("2")) Throw(new Exception("Server error")) else Return(Person("Ana", "Perez", "Bolivia", "La Paz", 30)) lazy val response1 = getPerson("http://server1.com/info") lazy val response2 = getPerson("http://server2.com/info") lazy val response3 = getPerson("http://server3.com/info") val response: Try[Person] = response1.rescue { case _ => response2.rescue { case _ => response3 } } println(response) // Return(Person(Ana,Perez,Bolivia,La Paz,30))
  • 23.
  • 24.
    Example 5 Obtain thefollowing information from a Binary Tree of Integers: ● The minimum value ● The maximum value ● The number of elements ● The number of elements greater than 5 ● Does it contain the number 20? ● Does it contain negative numbers? ● Are all elements positive numbers? ● What’s the first element greater than 5? ● Is the tree empty? ● Is the tree non empty? ● What’s the sum of the elements? ● What’s the product of the elements? ● What’s the reversed tree?
  • 25.
    Solution sealed trait BinaryTree[+A] objectBinaryTree { private final case class Leaf[A](value: A) extends BinaryTree[A] private final case class Branch[A](left: BinaryTree[A], right: BinaryTree[A]) extends BinaryTree[A] def leaf[A](value: A): BinaryTree[A] = BinaryTree.Leaf(value) def branch[A](left: BinaryTree[A], right: BinaryTree[A]): BinaryTree[A] = BinaryTree.Branch(left, right) }
  • 26.
    Solution sealed trait BinaryTree[+A]{ self => import BinaryTree._ def contains[A1 >: A](a: A1): Boolean = self.count(_ == a) > 0 def count(f: A => Boolean): Int = self match { case Leaf(a) => if (f(a)) 1 else 0 case Branch(l, r) => l.count(f) + r.count(f) } def exists(f: A => Boolean): Boolean = self.count(f) > 0 def find(f: A => Boolean): Option[A] = self match { case Leaf(a) => Some(a).filter(f) case Branch(l, r) => l.find(f).orElse(r.find(f)) } def fold[A1 >: A](f: (A1, A1) => A1): A1 = self match { case Leaf(a) => a case Branch(left, right) => f(left.fold(f), right.fold(f)) } def forall(f: A => Boolean): Boolean = self.count(f) == self.size def isEmpty: Boolean = self.size == 0 ... }
  • 27.
    Solution sealed trait BinaryTree[+A]{ self => import BinaryTree._ ... def map[B](f: A => B): BinaryTree[B] = self match { case Leaf(a) => Leaf(f(a)) case Branch(left, right) => Branch(left.map(f), right.map(f)) } def max[A1 >: A](implicit ordering: Ordering[A1]): A1 = self.maxBy(identity[A1]) def maxBy[B](f: A => B)(implicit ordering: Ordering[B]): A = self.fold(Ordering.by(f).max) def min[A1 >: A](implicit ordering: Ordering[A1]): A1 = self.minBy(identity[A1]) def minBy[B](f: A => B)(implicit ordering: Ordering[B]): A = self.fold(Ordering.by(f).min) def nonEmpty: Boolean = !self.isEmpty def product[A1 >: A](implicit numeric: Numeric[A1]): A1 = self.fold(numeric.times) def reverse: BinaryTree[A] = self match { case Leaf(a) => Leaf(a) case Branch(l, r) => Branch(r.reverse, l.reverse) } def sum[A1 >: A](implicit numeric: Numeric[A1]): A1 = self.fold(numeric.plus) def size: Int = self.count(_ => true) }
  • 28.
    val tree =branch( branch( branch( leaf(5), leaf(10) ), branch( leaf(7), leaf(8) ) ), branch( branch( leaf(1), leaf(11) ), branch( leaf(17), leaf(21) ) ) ) Solution println(s"Minimum: ${tree.min}") println(s"Maximum: ${tree.max}") println(s"Number of elements: ${tree.size}") println(s"Number of elements greater than 5: ${tree.count(_ > 5)}") println(s"Does the tree contain the number 20?: ${tree.contains(20)}") println(s"Does the tree contain negative numbers?: ${tree.exists(_ < 0)}") println(s"Are all elements in the tree positive numbers?: ${tree.forall(_ > 0)}") println(s"What's the first element greater than 5?: ${tree.find(_ > 5)}") println(s"Is the tree empty?: ${tree.isEmpty}") println(s"Is the tree non empty?: ${tree.nonEmpty}") println(s"What's the sum of the elements (1st approach)?: ${tree.fold(_ + _)}") println(s"What's the sum of the elements (2nd approach)?: ${tree.sum}") println(s"What's the product of the elements?: ${tree.product}") pprint.pprintln(s"Reversed tree: ${tree.reverse}")
  • 29.
  • 30.
    Example 6 Obtain thefollowing information from a Binary Tree of Person: ● Who’s the youngest person? ● Who’s the oldest person? ● What’s the average age? ● How many people are there per location?
  • 31.
    Solution sealed trait BinaryTree[+A]{ self => import BinaryTree._ ... def groupBy[K](f: A => K): Map[K, List[A]] = self match { case Leaf(a) => Map(f(a) -> List(a)) case Branch(l, r) => val leftMap = l.groupBy(f) val rightMap = r.groupBy(f) (leftMap.keySet ++ rightMap.keySet).map { key => (leftMap.get(key), rightMap.get(key)) match { case (Some(as1), Some(as2)) => key -> (as1 ++ as2) case (Some(as1), None) => key -> as1 case (None, Some(as2)) => key -> as2 case _ => throw new Error("Boom!") } }.toMap } ... }
  • 32.
    Solution val tree =branch( branch( branch( leaf(Person("Adam", "Peterson", "USA", "California", 40)), leaf(Person("Rachel", "Johns", "USA", "Los Angeles", 20)) ), branch( leaf(Person("Jose", "Perez", "Bolivia", "La Paz", 35)), leaf(Person("Monica", "Simpson", "UK", "London", 65)) ) ), branch( branch( leaf(Person("David", "Johnson", "USA", "California", 32)), leaf(Person("Ana", "Sanchez", "Bolivia", "La Paz", 27)) ), branch( leaf(Person("Laura", "Adams", "UK", "London", 54)), leaf(Person("Roberto", "Mendes", "Brazil", "Minas Gerais", 24)) ) ) ) println(s"Youngest person: ${tree.minBy(_.age)}") println(s"Oldest person: ${tree.maxBy(_.age)}") println(s"What's the average age?: ${tree.map(_.age).sum / tree.size}") println( s"How many people are there per location?: ${tree.groupBy(person => (person.country, person.state)).mapValues(_.length)}" )
  • 33.
    Example 7 Process aBinary Tree of Strings, sending them to a server which just echoes the received messages (encapsulated inside Future), and return a Future of a Binary Tree containing the responses.
  • 34.
    Solution sealed trait BinaryTree[+A]{ self => import BinaryTree._ ... def foreachFuture[B](f: A => Future[B]): Future[BinaryTree[B]] = self match { case Leaf(a) => f(a).map(Leaf(_)) case Branch(l, r) => l.foreachFuture(f).zipWith(r.foreachFuture(f))(Branch(_, _)) } ... }
  • 35.
    Solution def echo(message: String):Future[String] = Future { Thread.sleep(1000) s"Echo: $message" } val messages = branch( branch( leaf("message 1"), leaf("message 2") ), branch( branch( leaf("message 3"), leaf("message 4") ), branch( leaf("message 5"), leaf("message 6") ) ) ) val responses = messages.foreachFuture(echo) pprint.pprintln(Await.result(responses, 5.seconds)) /* Branch( Branch(Leaf("Echo: message 1"), Leaf("Echo: message 2")), Branch( Branch(Leaf("Echo: message 3"), Leaf("Echo: message 4")), Branch(Leaf("Echo: message 5"), Leaf("Echo: message 6")) ) ) */
  • 36.
  • 37.
  • 39.
  • 40.
    What is ZIOPrelude? Scala-first take on Functional Abstractions
  • 41.
    ZIO Prelude givesus... Data types that complement the Scala Standard Library: ● NonEmptyList ● NonEmptySet ● ZSet ● ZNonEmptySet ● Validation ● ZPure
  • 42.
    ZIO Prelude givesus... Newtypes that allow to increase type safety in domain modeling, wrapping an existing type without adding any runtime overhead.
  • 43.
    ZIO Prelude givesus... Typeclasses to describe similarities across different types, so we can eliminate duplication/boilerplate: ● Business entities (Person, ShoppingCart, etc.) ● Effect-like structures (Try, Option, Future, Either, etc.) ● Collection-like structures (List, Tree, etc.)
  • 44.
  • 45.
    Example 1 Let's supposewe have some cache stats data, organized by date and application. This data comes from two sources, and we need to combine them into one, by summing counters when collisions exist.
  • 46.
    Solution: Associative Typeclass traitAssociative[A] { def combine(l: => A, r: => A): A } // Associativity law (a1 <> (a2 <> a3)) <-> ((a1 <> a2) <> a3)
  • 47.
    Solution: Associative Typeclass ZIOPrelude has Associativeinstances for Scala Standard Types: ● Boolean ● Byte ● Char ● Short ● Int ● Long ● Float ● Double ● String ● Option ● Vector ● List ● Set ● Tuple
  • 48.
    Solution: Associative Typeclass And,of course, we can create instances of Associative for our own types (or types on third party libraries)!
  • 49.
    Solution: Associative Typeclass finalcase class CacheStats( entryCount: Option[Sum[Int]], memorySize: Option[Sum[Long]], hits: Option[Sum[Long]], misses: Option[Sum[Long]], loads: Option[Sum[Long]], evictions: Option[Sum[Long]] ) import zio.prelude._ object CacheStats { def make( entryCount: Option[Int], memorySize: Option[Long], hits: Option[Long], misses: Option[Long], loads: Option[Long], evictions: Option[Long] ): CacheStats = CacheStats( entryCount.map(Sum(_)), memorySize.map(Sum(_)), hits.map(Sum(_)), misses.map(Sum(_)), loads.map(Sum(_)), evictions.map(Sum(_)) ) implicit val associative = Associative.make[CacheStats] { (l, r) => CacheStats( l.entryCount <> r.entryCount, l.memorySize <> r.memorySize, l.hits <> r.hits, l.misses <> r.misses, l.loads <> r.loads, l.evictions <> r.evictions ) } }
  • 50.
  • 51.
    Solution: Associative Typeclass ●All typeclasses in ZIO Prelude include a set of laws that instances must obey. ● We can use ZIO Test to check that laws of a typeclass are fulfilled by a given instance, using Property Based Testing.
  • 52.
    Solution: Associative Typeclass objectCacheStatsSpec extends DefaultRunnableSpec { def spec = suite("CacheStatsSpec")( suite("CacheStats")( testM("associative")(checkAllLaws(Associative)(cacheStatsGen)) ) ) def cacheStatsGen[R <: Random with Sized]: Gen[R, CacheStats] = { val intGen = Gen.oneOf(Gen.none, Gen.anyInt.map(Sum(_)).map(Some(_))) val longGen = Gen.oneOf(Gen.none, Gen.anyLong.map(Sum(_)).map(Some(_))) for { entryCount <- intGen memorySize <- longGen hits <- longGen misses <- longGen loads <- longGen evictions <- longGen } yield CacheStats(entryCount, memorySize, hits, misses, loads, evictions) } }
  • 53.
    Solution: Associative Typeclass importzio.prelude._ pprint.pprintln( Associative[Map[LocalDate, Map[String, CacheStats]]].combine( stats1, stats2 ) ) /* Map( 2020-01-18 -> Map( "App X" -> CacheStats(Some(1000), Some(10000L), Some(4000L), Some(1000L), Some(10000L), Some(5000L)), "App Y" -> CacheStats(Some(800), Some(3500L), Some(6200L), None, Some(15780L), Some(3026L)), "App Z" -> CacheStats(None, Some(678L), Some(500L), None, Some(1600L), None) ), 2020-01-19 -> Map( "App A" -> CacheStats(None, None, Some(8196L), None, Some(10836L), None), "App B" -> CacheStats(Some(3134), Some(2854L), Some(8196L), Some(2000L), Some(10836L), Some(6000L)), "App C" -> CacheStats(None, None, Some(1000L), Some(467L), Some(1600L), None) ), 2020-03-05 -> Map( "App A" -> CacheStats(Some(8756), Some(1000L), Some(6420L), Some(2000L), None, None), "App Y" -> CacheStats(None, Some(1345L), Some(18064L), Some(246L), None, None) ), 2020-04-10 -> Map("App X" -> CacheStats(Some(1879), None, Some(864L), None, Some(5082L), None)) ) */ val stats1: Map[LocalDate, Map[String, CacheStats]] = Map( LocalDate.of(2020, 1, 18) -> Map( "App X" -> CacheStats.make(Some(1000), Some(5000), Some(2000), Some(500), Some(5000), Some(2500)), "App Y" -> CacheStats.make(None, Some(3500), Some(3100), None, Some(7890), Some(1513)), "App Z" -> CacheStats.make(None, None, Some(500), None, Some(800), None) ), LocalDate.of(2020, 1, 19) -> Map( "App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None), "App B" -> CacheStats.make(Some(1567), Some(2854), Some(4098), Some(1000), Some(5418), Some(3000)), "App C" -> CacheStats.make(None, None, Some(500), None, Some(800), None) ), LocalDate.of(2020, 3, 5) -> Map( "App A" -> CacheStats.make(Some(4378), Some(1000), Some(3210), Some(1000), None, None), "App Y" -> CacheStats.make(None, None, Some(9032), Some(123), None, None) ), LocalDate.of(2020, 4, 10) -> Map( "App X" -> CacheStats.make(Some(1879), None, Some(432), None, Some(2541), None) ) ) val stats2: Map[LocalDate, Map[String, CacheStats]] = Map( LocalDate.of(2020, 1, 18) -> Map( "App X" -> CacheStats.make(None, Some(5000), Some(2000), Some(500), Some(5000), Some(2500)), "App Y" -> CacheStats.make(Some(800), None, Some(3100), None, Some(7890), Some(1513)), "App Z" -> CacheStats.make(None, Some(678), None, None, Some(800), None) ), LocalDate.of(2020, 1, 19) -> Map( "App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None), "App B" -> CacheStats.make(Some(1567), None, Some(4098), Some(1000), Some(5418), Some(3000)), "App C" -> CacheStats.make(None, None, Some(500), Some(467), Some(800), None) ), LocalDate.of(2020, 3, 5) -> Map( "App A" -> CacheStats.make(Some(4378), None, Some(3210), Some(1000), None, None), "App Y" -> CacheStats.make(None, Some(1345), Some(9032), Some(123), None, None) ), LocalDate.of(2020, 4, 10) -> Map( "App X" -> CacheStats.make(None, None, Some(432), None, Some(2541), None) ) )
  • 54.
    Solution: Associative Typeclass importzio.prelude._ pprint.pprintln(stats1 <> stats2) /* Map( 2020-01-18 -> Map( "App X" -> CacheStats(Some(1000), Some(10000L), Some(4000L), Some(1000L), Some(10000L), Some(5000L)), "App Y" -> CacheStats(Some(800), Some(3500L), Some(6200L), None, Some(15780L), Some(3026L)), "App Z" -> CacheStats(None, Some(678L), Some(500L), None, Some(1600L), None) ), 2020-01-19 -> Map( "App A" -> CacheStats(None, None, Some(8196L), None, Some(10836L), None), "App B" -> CacheStats(Some(3134), Some(2854L), Some(8196L), Some(2000L), Some(10836L), Some(6000L)), "App C" -> CacheStats(None, None, Some(1000L), Some(467L), Some(1600L), None) ), 2020-03-05 -> Map( "App A" -> CacheStats(Some(8756), Some(1000L), Some(6420L), Some(2000L), None, None), "App Y" -> CacheStats(None, Some(1345L), Some(18064L), Some(246L), None, None) ), 2020-04-10 -> Map("App X" -> CacheStats(Some(1879), None, Some(864L), None, Some(5082L), None)) ) */ val stats1: Map[LocalDate, Map[String, CacheStats]] = Map( LocalDate.of(2020, 1, 18) -> Map( "App X" -> CacheStats.make(Some(1000), Some(5000), Some(2000), Some(500), Some(5000), Some(2500)), "App Y" -> CacheStats.make(None, Some(3500), Some(3100), None, Some(7890), Some(1513)), "App Z" -> CacheStats.make(None, None, Some(500), None, Some(800), None) ), LocalDate.of(2020, 1, 19) -> Map( "App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None), "App B" -> CacheStats.make(Some(1567), Some(2854), Some(4098), Some(1000), Some(5418), Some(3000)), "App C" -> CacheStats.make(None, None, Some(500), None, Some(800), None) ), LocalDate.of(2020, 3, 5) -> Map( "App A" -> CacheStats.make(Some(4378), Some(1000), Some(3210), Some(1000), None, None), "App Y" -> CacheStats.make(None, None, Some(9032), Some(123), None, None) ), LocalDate.of(2020, 4, 10) -> Map( "App X" -> CacheStats.make(Some(1879), None, Some(432), None, Some(2541), None) ) ) val stats2: Map[LocalDate, Map[String, CacheStats]] = Map( LocalDate.of(2020, 1, 18) -> Map( "App X" -> CacheStats.make(None, Some(5000), Some(2000), Some(500), Some(5000), Some(2500)), "App Y" -> CacheStats.make(Some(800), None, Some(3100), None, Some(7890), Some(1513)), "App Z" -> CacheStats.make(None, Some(678), None, None, Some(800), None) ), LocalDate.of(2020, 1, 19) -> Map( "App A" -> CacheStats.make(None, None, Some(4098), None, Some(5418), None), "App B" -> CacheStats.make(Some(1567), None, Some(4098), Some(1000), Some(5418), Some(3000)), "App C" -> CacheStats.make(None, None, Some(500), Some(467), Some(800), None) ), LocalDate.of(2020, 3, 5) -> Map( "App A" -> CacheStats.make(Some(4378), None, Some(3210), Some(1000), None, None), "App Y" -> CacheStats.make(None, Some(1345), Some(9032), Some(123), None, None) ), LocalDate.of(2020, 4, 10) -> Map( "App X" -> CacheStats.make(None, None, Some(432), None, Some(2541), None) ) )
  • 55.
  • 56.
    Example 2 Let's supposewe have a List of cache stats data, and we want to sum all stats.
  • 57.
    Solution: Identity Typeclass traitIdentity[A] extends Associative[A] { def identity: A } // Associativity law (a1 <> (a2 <> a3)) <-> ((a1 <> a2) <> a3) // Left Identity law (identity <> a) <-> a // Right Identity law (a <> identity) <-> a
  • 58.
    Solution: Identity Typeclass finalcase class CacheStats( entryCount: Option[Sum[Int]], memorySize: Option[Sum[Long]], hits: Option[Sum[Long]], misses: Option[Sum[Long]], loads: Option[Sum[Long]], evictions: Option[Sum[Long]] ) import zio.prelude._ object CacheStats { def make( entryCount: Option[Int], memorySize: Option[Long], hits: Option[Long], misses: Option[Long], loads: Option[Long], evictions: Option[Long] ): CacheStats = CacheStats( entryCount.map(Sum(_)), memorySize.map(Sum(_)), hits.map(Sum(_)), misses.map(Sum(_)), loads.map(Sum(_)), evictions.map(Sum(_)) ) implicit val identity = Identity.make[CacheStats]( CacheStats.make(None, None, None, None, None, None), (l, r) => CacheStats( l.entryCount <> r.entryCount, l.memorySize <> r.memorySize, l.hits <> r.hits, l.misses <> r.misses, l.loads <> r.loads, l.evictions <> r.evictions ) ) }
  • 59.
    Solution: Identity Typeclass defsum(cacheStats: List[CacheStats]): CacheStats = cacheStats.foldRight(Identity[CacheStats].identity)(_ <> _) def main(args: Array[String]): Unit = { val cacheStats1 = List( CacheStats.make(Some(1000), Some(5000), Some(2000), Some(500), Some(5000), Some(2500)), CacheStats.make(None, Some(3500), Some(3100), None, Some(7890), Some(1513)), CacheStats.make(None, None, Some(500), None, Some(800), None) ) val cacheStats2 = List.empty[CacheStats] println(sum(cacheStats1)) // CacheStats(Some(1000), Some(8500L), Some(5600L), Some(500L), Some(13690L), Some(4013L)) println(sum(cacheStats2)) // CacheStats(None, None, None, None, None, None) }
  • 60.
  • 61.
    Example 3 Let's supposewe have several Options and we want to combine them into an Option of a Tuple
  • 62.
    Solution: AssociativeBoth Typeclass traitAssociativeBoth[F[_]] { def both[A, B](fa: => F[A], fb: => F[B]): F[(A, B)] } // Associativity law both(fa, both(fb, fc)) ~ both(both(fa, fb), fc)
  • 63.
    Solution: AssociativeBoth Typeclass ZIOPrelude has AssociativeBothinstances for Scala Standard Types: ● Either ● Future ● List ● Option ● Try ● Vector
  • 64.
    Solution: AssociativeBoth Typeclass importzio.prelude._ def main(args: Array[String]): Unit = { val option1 = Option(1) val option2 = Option(2) val option3 = Option(3) val option4 = Option(4) val option5 = Option(5) val option6 = Option(6) val option7 = Option(7) val option8 = Option(8) val option9 = Option(9) val option10 = Option(10) println { AssociativeBoth.tupleN(option1, option2, option3, option4, option5, option6, option7, option8, option9, option10) } // Some(Tuple10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) }
  • 65.
  • 66.
    Example 4 Let's supposewe request some Person data from three different servers, and we want to return the first successful response, or a failure if all the requests fail
  • 67.
    Solution: AssociativeEither Typeclass traitAssociativeEither[F[_]] { def either[A, B](fa: => F[A], fb: => F[B]): F[Either[A, B]] } // Associativity law either(fa, either(fb, fc)) ~ either(either(fa, fb), fc)
  • 68.
    Solution: AssociativeEither Typeclass importcom.twitter.util.{ Return, Throw, Try } import zio.prelude._ implicit val TryAssociativeEither = new AssociativeEither[Try] { def either[A, B](fa: => Try[A], fb: => Try[B]): Try[Either[A, B]] = fa.map(Left(_)) rescue { case _ => fb.map(Right(_)) } }
  • 69.
    Solution: AssociativeEither Typeclass importcom.twitter.util.{ Return, Throw, Try } import zio.prelude._ final case class Person(firstName: String, lastName: String, country: String, state: String, age: Int) def getPerson(url: String): Try[Person] = if (url.contains("1") || url.contains("2")) Throw(new Exception("Server error")) else Return(Person("Ana", "Perez", "Bolivia", "La Paz", 30)) def main(args: Array[String]): Unit = { lazy val response1 = getPerson("http://server1.com/info") lazy val response2 = getPerson("http://server2.com/info") lazy val response3 = getPerson("http://server3.com/info") val response: Try[Person] = response1 orElse response2 orElse response3 println(response) // Return(Person("Ana", "Perez", "Bolivia", "La Paz", 30)) }
  • 70.
  • 71.
    Example 5 Obtain thefollowing information from a Binary Tree of Integers: ● The minimum value ● The maximum value ● The number of elements ● The number of elements greater than 5 ● Does it contain the number 20? ● Does it contain negative numbers? ● Are all elements positive numbers? ● What’s the first element greater than 5? ● Is the tree empty? ● Is the tree non empty? ● What’s the sum of the elements? ● What’s the product of the elements? ● What’s the reversed tree?
  • 72.
    Solution: Traversable Typeclass traitTraversable[F[+_]] extends Covariant[F] { def foreach[G[+_]: IdentityBoth: Covariant, A, B](fa: F[A])(f: A => G[B]): G[F[B]] // A lot of methods for free! def contains[A, A1 >: A](fa: F[A])(a: A1)(implicit A: Equal[A1]): Boolean def count[A](fa: F[A])(f: A => Boolean): Int def exists[A](fa: F[A])(f: A => Boolean): Boolean def find[A](fa: F[A])(f: A => Boolean): Option[A] def flip[G[+_]: IdentityBoth: Covariant, A](fa: F[G[A]]): G[F[A]] def fold[A: Identity](fa: F[A]): A def foldLeft[S, A](fa: F[A])(s: S)(f: (S, A) => S): S def foldMap[A, B: Identity](fa: F[A])(f: A => B): B def foldRight[S, A](fa: F[A])(s: S)(f: (A, S) => S): S def forall[A](fa: F[A])(f: A => Boolean): Boolean def foreach_[G[+_]: IdentityBoth: Covariant, A](fa: F[A])(f: A => G[Any]): G[Unit] def isEmpty[A](fa: F[A]): Boolean def map[A, B](f: A => B): F[A] => F[B] def mapAccum[S, A, B](fa: F[A])(s: S)(f: (S, A) => (S, B)): (S, F[B]) def maxOption[A: Ord](fa: F[A]): Option[A] def maxByOption[A, B: Ord](fa: F[A])(f: A => B): Option[A] def minOption[A: Ord](fa: F[A]): Option[A] def minByOption[A, B: Ord](fa: F[A])(f: A => B): Option[A] def nonEmpty[A](fa: F[A]): Boolean def product[A](fa: F[A])(implicit ev: Identity[Prod[A]]): A def reduceMapOption[A, B: Associative](fa: F[A])(f: A => B): Option[B] def reduceOption[A](fa: F[A])(f: (A, A) => A): Option[A] def reverse[A](fa: F[A]): F[A] def size[A](fa: F[A]): Int def sum[A](fa: F[A])(implicit ev: Identity[Sum[A]]): A def toChunk[A](fa: F[A]): Chunk[A] def toList[A](fa: F[A]): List[A] def zipWithIndex[A](fa: F[A]): F[(A, Int)] }
  • 73.
    Solution: Traversable Typeclass sealedtrait BinaryTree[+A] { self => import BinaryTree._ def map[B](f: A => B): BinaryTree[B] = self match { case Leaf(a) => Leaf(f(a)) case Branch(left, right) => Branch(left.map(f), right.map(f)) } } object BinaryTree { private final case class Leaf[A](value: A) extends BinaryTree[A] private final case class Branch[A](left: BinaryTree[A], right: BinaryTree[A]) extends BinaryTree[A] def leaf[A](value: A): BinaryTree[A] = BinaryTree.Leaf(value) def branch[A](left: BinaryTree[A], right: BinaryTree[A]): BinaryTree[A] = BinaryTree.Branch(left, right) implicit val traversable = new Traversable[BinaryTree] { def foreach[G[+ _]: IdentityBoth: Covariant, A, B](fa: BinaryTree[A])(f: A => G[B]): G[BinaryTree[B]] = fa match { case Leaf(a) => f(a).map(Leaf(_)) case Branch(l, r) => foreach(l)(f).zipWith(foreach(r)(f))(Branch(_, _)) } override def map[A, B](f: A => B): BinaryTree[A] => BinaryTree[B] = _.map(f) } }
  • 74.
    val tree =branch( branch( branch( leaf(5), leaf(10) ), branch( leaf(7), leaf(8) ) ), branch( branch( leaf(1), leaf(11) ), branch( leaf(17), leaf(21) ) ) ) Solution: Traversable Typeclass println(s"Minimum: ${tree.minOption}") println(s"Maximum: ${tree.maxOption}") println(s"Number of elements: ${tree.size}") println(s"Number of elements greater than 5: ${tree.count(_ > 5)}") println(s"Does the tree contain the number 20?: ${tree.contains(20)}") println(s"Does the tree contain negative numbers?: ${tree.exists(_ < 0)}") println(s"Are all elements in the tree positive numbers?: ${tree.forall(_ > 0)}") println(s"What's the first element greater than 5?: ${tree.find(_ > 5)}") println(s"Is the tree empty?: ${tree.isEmpty}") println(s"Is the tree non empty?: ${tree.nonEmpty}") println(s"What's the sum of the elements: ${tree.sum}") println(s"What's the product of the elements?: ${tree.product}") pprint.pprintln(s"Reversed tree: ${tree.reverse}")
  • 75.
  • 76.
    Example 6 Obtain thefollowing information from a Binary Tree of Person: ● Who’s the youngest person? ● Who’s the oldest person? ● What’s the average age? ● How many people are there per location?
  • 77.
    Solution: Traversable Typeclass valtree = branch( branch( branch( leaf(Person("Adam", "Peterson", "USA", "California", 40)), leaf(Person("Rachel", "Johns", "USA", "Los Angeles", 20)) ), branch( leaf(Person("Jose", "Perez", "Bolivia", "La Paz", 35)), leaf(Person("Monica", "Simpson", "UK", "London", 65)) ) ), branch( branch( leaf(Person("David", "Johnson", "USA", "California", 32)), leaf(Person("Ana", "Sanchez", "Bolivia", "La Paz", 27)) ), branch( leaf(Person("Laura", "Adams", "UK", "London", 54)), leaf(Person("Roberto", "Mendes", "Brazil", "Minas Gerais", 24)) ) ) ) println(s"Youngest person: ${tree.minByOption(_.age)}") println(s"Oldest person: ${tree.maxByOption(_.age)}") println(s"What's the average age?: ${tree.map(_.age).sum / tree.size}") println( s"How many people are there per location?: ${Traversable[BinaryTree].groupBy(tree)(person => (person.country, person.state)).mapValues(_.length)}" )
  • 79.
    Solution: NonEmptyTraversable Typeclass traitNonEmptyTraversable[F[+_]] extends Traversable[F] { def foreach1[G[+_]: AssociativeBoth: Covariant, A, B](fa: F[A])(f: A => G[B]): G[F[B]] def flip1[G[+_]: AssociativeBoth: Covariant, A](fa: F[G[A]]): G[F[A]] override def foreach[G[+_]: IdentityBoth: Covariant, A, B](fa: F[A])(f: A => G[B]): G[F[B]] def foreach1_[G[+_]: AssociativeBoth: Covariant, A](fa: F[A])(f: A => G[Any]): G[Unit] def max[A: Ord](fa: F[A]): A def maxBy[A, B: Ord](fa: F[A])(f: A => B): A def min[A: Ord](fa: F[A]): A def minBy[A, B: Ord](fa: F[A])(f: A => B): A def reduce[A](fa: F[A])(f: (A, A) => A): A def reduce1[A: Associative](fa: F[A]): A def reduceMap[A, B: Associative](fa: F[A])(f: A => B): B def reduceMapLeft[A, B](fa: F[A])(map: A => B)(reduce: (B, A) => B): B def reduceMapRight[A, B](fa: F[A])(map: A => B)(reduce: (A, B) => B): B def toNonEmptyChunk[A](fa: F[A]): NonEmptyChunk[A] def toNonEmptyList[A](fa: F[A]): NonEmptyList[A] }
  • 80.
    Solution: NonEmptyTraversable Typeclass sealedtrait BinaryTree[+A] { self => import BinaryTree._ def map[B](f: A => B): BinaryTree[B] = self match { case Leaf(a) => Leaf(f(a)) case Branch(left, right) => Branch(left.map(f), right.map(f)) } } object BinaryTree { private final case class Leaf[A](value: A) extends BinaryTree[A] private final case class Branch[A](left: BinaryTree[A], right: BinaryTree[A]) extends BinaryTree[A] def leaf[A](value: A): BinaryTree[A] = BinaryTree.Leaf(value) def branch[A](left: BinaryTree[A], right: BinaryTree[A]): BinaryTree[A] = BinaryTree.Branch(left, right) implicit val nonEmptyTraversable = new NonEmptyTraversable[BinaryTree] { def foreach1[G[+ _]: AssociativeBoth: Covariant, A, B](fa: BinaryTree[A])(f: A => G[B]): G[BinaryTree[B]] = fa match { case Leaf(a) => f(a).map(Leaf(_)) case Branch(l, r) => foreach1(l)(f).zipWith(foreach1(r)(f))(Branch(_, _)) } } }
  • 81.
    Solution: NonEmptyTraversable Typeclass valtree = branch( branch( branch( leaf(Person("Adam", "Peterson", "USA", "California", 40)), leaf(Person("Rachel", "Johns", "USA", "Los Angeles", 20)) ), branch( leaf(Person("Jose", "Perez", "Bolivia", "La Paz", 35)), leaf(Person("Monica", "Simpson", "UK", "London", 65)) ) ), branch( branch( leaf(Person("David", "Johnson", "USA", "California", 32)), leaf(Person("Ana", "Sanchez", "Bolivia", "La Paz", 27)) ), branch( leaf(Person("Laura", "Adams", "UK", "London", 54)), leaf(Person("Roberto", "Mendes", "Brazil", "Minas Gerais", 24)) ) ) ) println(s"Youngest person: ${NonEmptyTraversable[BinaryTree].minBy(tree)(_.age)}") println(s"Oldest person: ${NonEmptyTraversable[BinaryTree].maxBy(tree)(_.age)}")
  • 82.
  • 83.
    Example 7 Process aBinary Tree of Strings, sending them to a server which just echoes the received messages (encapsulated inside Future), and return a Future of a Binary Tree containing the responses.
  • 84.
    Solution: Traversable Typeclass defecho(message: String): Future[String] = Future { Thread.sleep(1000) s"Echo: $message" } val messages = branch( branch( leaf("message 1"), leaf("message 2") ), branch( branch( leaf("message 3"), leaf("message 4") ), branch( leaf("message 5"), leaf("message 6") ) ) ) val responses = messages.foreach(echo) pprint.pprintln(Await.result(responses, 5.seconds)) /* Branch( Branch(Leaf("Echo: message 1"), Leaf("Echo: message 2")), Branch( Branch(Leaf("Echo: message 3"), Leaf("Echo: message 4")), Branch(Leaf("Echo: message 5"), Leaf("Echo: message 6")) ) ) */
  • 85.
  • 86.
    Summary ● There's alot of boilerplate in our applications ● Abstraction is the key to eliminating boilerplate ● Functional code uses typeclasses for abstraction
  • 87.
    Summary ZIO Prelude hastypeclasses that let you eliminate boilerplate across: ● Business entities: ○ Associative/Identity ● Effect-like structures: ○ AssociativeBoth/IdentityBoth ○ AssociativeEither/IdentityEither ● Collection-like structures: ○ Traversable/NonEmptyTraversable
  • 88.
    Special thanks ● ToFunctional Scala organizers for hosting this presentation ● To John De Goes for guidance and support
  • 89.
  • 90.
    Where to learnmore ● ZIO Prelude on Github: https://github.com/zio/zio-prelude/ ● SF Scala: Reimagining Functional Type Classes, talk by John De Goes and Adam Fraser ● Functional World: Exploring ZIO Prelude - The game changer for typeclasses in Scala, talk by Jorge Vásquez
  • 91.