SlideShare a Scribd company logo
The Terror-Free Guide to
Introducing Functional Scala at Work
(without dying in the process)
JORGE VÁSQUEZ
SCALA DEVELOPER
The Terror-Free Guide to Introducing Functional Scala at Work
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
The Terror-Free Guide to Introducing Functional Scala at Work
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)}"
)
The Terror-Free Guide to Introducing Functional Scala at Work
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

More Related Content

PDF
Refactoring Functional Type Classes
PDF
素数の分解法則(フロベニウスやばい) #math_cafe
PDF
ゲームAI製作のためのワークショップ(I)
PDF
Channel
PPTX
WebSocket protocol
PDF
Error Management: Future vs ZIO
PDF
One Monad to Rule Them All
PDF
The Functional Programmer's Toolkit (NDC London 2019)
Refactoring Functional Type Classes
素数の分解法則(フロベニウスやばい) #math_cafe
ゲームAI製作のためのワークショップ(I)
Channel
WebSocket protocol
Error Management: Future vs ZIO
One Monad to Rule Them All
The Functional Programmer's Toolkit (NDC London 2019)

What's hot (20)

PDF
AIWolfPy v0.4.9
PDF
[Golang] 以 Mobile App 工程師視角,帶你進入 Golang 的世界 (Introduction of GoLang)
PDF
深入淺出C語言
PDF
Qt Internationalization
 
PDF
Introduction to the Qt Quick Scene Graph
 
PDF
A Scala Corrections Library
PDF
Zio in real world
PPT
Atm traffic management geekssay.com
PDF
[ZigBee 嵌入式系統] ZigBee 應用實作 - 使用 TI Z-Stack Firmware
PDF
一人でもNFC開発
PDF
Exploring ZIO Prelude: The game changer for typeclasses in Scala
PDF
Coq関係計算ライブラリの開発と写像の性質の証明
PDF
Basics of Model/View Qt programming
 
PDF
PyData Berlin 2018: dvc.org
PDF
怎樣在 Flutter app 中使用 Google Maps
PDF
An introduction to property based testing
PDF
Rデータフレーム自由自在
PDF
Pythonの処理系はどのように実装され,どのように動いているのか? 我々はその実態を調査すべくアマゾンへと飛んだ.
PDF
Techniques of integration
AIWolfPy v0.4.9
[Golang] 以 Mobile App 工程師視角,帶你進入 Golang 的世界 (Introduction of GoLang)
深入淺出C語言
Qt Internationalization
 
Introduction to the Qt Quick Scene Graph
 
A Scala Corrections Library
Zio in real world
Atm traffic management geekssay.com
[ZigBee 嵌入式系統] ZigBee 應用實作 - 使用 TI Z-Stack Firmware
一人でもNFC開発
Exploring ZIO Prelude: The game changer for typeclasses in Scala
Coq関係計算ライブラリの開発と写像の性質の証明
Basics of Model/View Qt programming
 
PyData Berlin 2018: dvc.org
怎樣在 Flutter app 中使用 Google Maps
An introduction to property based testing
Rデータフレーム自由自在
Pythonの処理系はどのように実装され,どのように動いているのか? 我々はその実態を調査すべくアマゾンへと飛んだ.
Techniques of integration
Ad

Similar to The Terror-Free Guide to Introducing Functional Scala at Work (20)

ODP
Introduction To PostGIS
ODP
FOSS4G 2010 PostGIS Raster: an Open Source alternative to Oracle GeoRaster
PPTX
Mapping For Sharepoint T11 Peter Smith
PDF
Analytics with Spark
PDF
Hadoop I/O Analysis
PDF
Seeing Like Software
PDF
JS Fest 2019. Anjana Vakil. Serverless Bebop
ODP
Stratosphere Intro (Java and Scala Interface)
PDF
2016 gunma.web games-and-asm.js
PDF
Pycon2011
PDF
A Rusty introduction to Apache Arrow and how it applies to a time series dat...
KEY
Saving Gaia with GeoDjango
PDF
10. Kapusta, Stofanak - eGlu
PPTX
Python en la Plataforma ArcGIS
PDF
Eric Lafortune - The Jack and Jill build system
PDF
Wprowadzenie do technologi Big Data i Apache Hadoop
PDF
Refactoring to Macros with Clojure
PDF
Easy GPS Tracker using Arduino and Python
PPT
JQuery Flot
PDF
A More Flash Like Web?
Introduction To PostGIS
FOSS4G 2010 PostGIS Raster: an Open Source alternative to Oracle GeoRaster
Mapping For Sharepoint T11 Peter Smith
Analytics with Spark
Hadoop I/O Analysis
Seeing Like Software
JS Fest 2019. Anjana Vakil. Serverless Bebop
Stratosphere Intro (Java and Scala Interface)
2016 gunma.web games-and-asm.js
Pycon2011
A Rusty introduction to Apache Arrow and how it applies to a time series dat...
Saving Gaia with GeoDjango
10. Kapusta, Stofanak - eGlu
Python en la Plataforma ArcGIS
Eric Lafortune - The Jack and Jill build system
Wprowadzenie do technologi Big Data i Apache Hadoop
Refactoring to Macros with Clojure
Easy GPS Tracker using Arduino and Python
JQuery Flot
A More Flash Like Web?
Ad

More from Jorge Vásquez (9)

PDF
Behold! The Happy Path To Captivate Your Users With Stunning CLI Apps!
PDF
Programación Funcional 101 con Scala y ZIO 2.0
PDF
A Prelude of Purity: Scaling Back ZIO
PDF
Be Smart, Constrain Your Types to Free Your Brain!
PDF
Consiguiendo superpoderes para construir aplicaciones modernas en la JVM con ZIO
PDF
Functional Programming 101 with Scala and ZIO @FunctionalWorld
PDF
ZIO Prelude - ZIO World 2021
PDF
Exploring type level programming in Scala
PDF
Introduction to programming with ZIO functional effects
Behold! The Happy Path To Captivate Your Users With Stunning CLI Apps!
Programación Funcional 101 con Scala y ZIO 2.0
A Prelude of Purity: Scaling Back ZIO
Be Smart, Constrain Your Types to Free Your Brain!
Consiguiendo superpoderes para construir aplicaciones modernas en la JVM con ZIO
Functional Programming 101 with Scala and ZIO @FunctionalWorld
ZIO Prelude - ZIO World 2021
Exploring type level programming in Scala
Introduction to programming with ZIO functional effects

Recently uploaded (20)

PPTX
VVF-Customer-Presentation2025-Ver1.9.pptx
PPTX
Agentic AI Use Case- Contract Lifecycle Management (CLM).pptx
PDF
Navsoft: AI-Powered Business Solutions & Custom Software Development
PDF
Design an Analysis of Algorithms II-SECS-1021-03
PPTX
Essential Infomation Tech presentation.pptx
PDF
Digital Strategies for Manufacturing Companies
PDF
Claude Code: Everyone is a 10x Developer - A Comprehensive AI-Powered CLI Tool
PDF
SAP S4 Hana Brochure 3 (PTS SYSTEMS AND SOLUTIONS)
PPTX
Reimagine Home Health with the Power of Agentic AI​
PPTX
Introduction to Artificial Intelligence
PDF
Addressing The Cult of Project Management Tools-Why Disconnected Work is Hold...
PPTX
CHAPTER 2 - PM Management and IT Context
PDF
Nekopoi APK 2025 free lastest update
PDF
EN-Survey-Report-SAP-LeanIX-EA-Insights-2025.pdf
PDF
PTS Company Brochure 2025 (1).pdf.......
PDF
Wondershare Filmora 15 Crack With Activation Key [2025
PDF
medical staffing services at VALiNTRY
PPTX
ai tools demonstartion for schools and inter college
PDF
Odoo Companies in India – Driving Business Transformation.pdf
PDF
AI in Product Development-omnex systems
VVF-Customer-Presentation2025-Ver1.9.pptx
Agentic AI Use Case- Contract Lifecycle Management (CLM).pptx
Navsoft: AI-Powered Business Solutions & Custom Software Development
Design an Analysis of Algorithms II-SECS-1021-03
Essential Infomation Tech presentation.pptx
Digital Strategies for Manufacturing Companies
Claude Code: Everyone is a 10x Developer - A Comprehensive AI-Powered CLI Tool
SAP S4 Hana Brochure 3 (PTS SYSTEMS AND SOLUTIONS)
Reimagine Home Health with the Power of Agentic AI​
Introduction to Artificial Intelligence
Addressing The Cult of Project Management Tools-Why Disconnected Work is Hold...
CHAPTER 2 - PM Management and IT Context
Nekopoi APK 2025 free lastest update
EN-Survey-Report-SAP-LeanIX-EA-Insights-2025.pdf
PTS Company Brochure 2025 (1).pdf.......
Wondershare Filmora 15 Crack With Activation Key [2025
medical staffing services at VALiNTRY
ai tools demonstartion for schools and inter college
Odoo Companies in India – Driving Business Transformation.pdf
AI in Product Development-omnex systems

The Terror-Free Guide to Introducing Functional Scala at Work

  • 1. The Terror-Free Guide to Introducing Functional Scala at Work (without dying in the process)
  • 4. We are hiring Scala Developers! https://scalac.io/careers/
  • 5. Agenda ● Motivation ● ZIO Prelude Overview ● How ZIO Prelude can help us
  • 7. 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.
  • 9. 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) }
  • 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) ) )
  • 13. 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) ) } }
  • 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 suppose we have a List of cache stats data, and we want to sum all stats.
  • 17. 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) ) } }
  • 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 suppose we 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 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
  • 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))
  • 24. 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?
  • 25. 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) }
  • 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}")
  • 30. 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?
  • 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 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.
  • 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")) ) ) */
  • 40. What is ZIO Prelude? Scala-first take on Functional Abstractions
  • 41. ZIO Prelude gives us... Data types that complement the Scala Standard Library: ● NonEmptyList ● NonEmptySet ● ZSet ● ZNonEmptySet ● Validation ● ZPure
  • 42. ZIO Prelude gives us... Newtypes that allow to increase type safety in domain modeling, wrapping an existing type without adding any runtime overhead.
  • 43. 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.)
  • 45. 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.
  • 46. Solution: Associative Typeclass trait Associative[A] { def combine(l: => A, r: => A): A } // Associativity law (a1 <> (a2 <> a3)) <-> ((a1 <> a2) <> a3)
  • 47. 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
  • 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 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 ) } }
  • 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 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) } }
  • 53. 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) ) )
  • 54. 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) ) )
  • 56. Example 2 Let's suppose we have a List of cache stats data, and we want to sum all stats.
  • 57. 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
  • 58. 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 ) ) }
  • 59. 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) }
  • 61. Example 3 Let's suppose we have several Options and we want to combine them into an Option of a Tuple
  • 62. 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)
  • 63. Solution: AssociativeBoth Typeclass ZIO Prelude has AssociativeBothinstances for Scala Standard Types: ● Either ● Future ● List ● Option ● Try ● Vector
  • 64. 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)) }
  • 66. 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
  • 67. 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)
  • 68. 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(_)) } }
  • 69. 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)) }
  • 71. 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?
  • 72. 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)] }
  • 73. 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) } }
  • 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}")
  • 76. 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?
  • 77. 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)}" )
  • 79. 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] }
  • 80. 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(_, _)) } } }
  • 81. 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)}")
  • 83. 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.
  • 84. 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")) ) ) */
  • 86. Summary ● There's a lot of boilerplate in our applications ● Abstraction is the key to eliminating boilerplate ● Functional code uses typeclasses for abstraction
  • 87. 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
  • 88. Special thanks ● To Functional Scala organizers for hosting this presentation ● To John De Goes for guidance and support
  • 90. 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