Exploring ZIO Prelude:
The Game-Changer for
Typeclasses in Scala
JORGE VASQUEZ
SCALA DEVELOPER
Agenda
● Motivations around ZIO Prelude
● Tour of ZIO Prelude
○ Validating data
○ Combining data structures
○ Traversing data structures
○ Executing pure computations
Motivations
around
ZIO Prelude
Background
● Functional Scala was born in Haskell.
● Scalaz took proven machinery in Haskell, and ported it to
Scala.
● Scalaz has been forked and replicated into other libraries.
But Scala is not Haskell!
Scala combines object-oriented and functional programming
in one concise, high-level language.
But Scala is not Haskell!
Scala does not have everything Haskell has:
● Lack of full type inference
● Adds more performance overhead
But Scala is not Haskell!
Scala has things Haskell does not have!
● Powerful subtyping
● Powerful declaration-site variance
In conclusion...
We need a Scala-first take on functional abstractions:
ZIO Prelude gives us that!
ZIO Prelude gives us...
● Modular hierarchy that relies on subtyping
● Declaration-site variance
● Great type inference
ZIO Prelude gives us...
● No Haskell-isms!
○ ap on Applicative
○ Over reliance on Unit
○ Invariance
ZIO Prelude gives us...
● Minimal implicits
● Less indirection
Tour of ZIO Prelude
ZIO Prelude
● ZIO Prelude is a small library
● Today we will cover a smaller part
ZIO Prelude on GitHub
https://github.com/zio/zio-prelude/
Scenario 1:
Validating
data
Example
Validate Person data:
● First name must not be empty
● Last name must not be empty
● Age must not be negative
Take 1: Either
sealed abstract case class Person private (firstName: String, lastName: String, age: Int)
object Person {
def make(firstName: String, lastName: String, age: Int): Either[String, Person] =
for {
validFirstName <- validateFirstName(firstName)
validLastName <- validateLastName(lastName)
validAge <- validateAge(age)
} yield new Person(validFirstName, validLastName, validAge) {}
private def validateFirstName(firstName: String): Either[String, String] =
if (firstName.nonEmpty) Right(firstName)
else Left("First name must not be empty")
private def validateLastName(lastName: String): Either[String, String] =
if (lastName.nonEmpty) Right(lastName)
else Left("Last name must not be empty")
private def validateAge(age: Int): Either[String, Int] =
if (age >= 0) Right(age)
else Left("Age must not be negative")
}
Take 1: Either
val validPerson = Person.make("Clark", "Kent", 30)
println(s"Valid Person: $validPerson")
// Valid Person: Right(Person(Clark,Kent,30))
val invalidPerson = Person.make("", "", -1)
println(s"Invalid Person: $invalidPerson")
// Invalid Person: Left(First name must not be empty)
Problems with Take 1: Either
● Short-circuiting on failure
● If validation errors exist, we just get the first one!
Take 2: Either
def make(firstName: String, lastName: String, age: Int): Either[List[String], Person] =
validateFirstName(firstName) match {
case Left(errorFirstName) =>
validateLastName(lastName) match {
case Left(errorLastName) =>
validateAge(age) match {
case Left(errorAge) => Left(List(errorFirstName, errorLastName, errorAge))
case Right(_) => Left(List(errorFirstName, errorLastName))
}
case Right(_) =>
validateAge(age) match {
case Left(error2) => Left(List(errorFirstName, error2))
case Right(_) => Left(List(errorFirstName))
}
}
case Right(firstName) =>
validateLastName(lastName) match {
case Left(errorLastName) =>
validateAge(age) match {
case Left(errorAge) => Left(List(errorLastName, errorAge))
case Right(_) => Left(List(errorLastName))
}
case Right(lastName) =>
validateAge(age) match {
case Left(errorAge) => Left(List(errorAge))
case Right(age) => Right(new Person(firstName, lastName, age) {})
}
}
}
Take 2: Either
val validPerson = Person.make("Clark", "Kent", 30)
println(s"Valid Person: $validPerson")
// Valid Person: Right(Person(Clark,Kent,30))
val invalidPerson = Person.make("", "", -1)
println(s"Invalid Person: $invalidPerson")
// Invalid Person: Left(List(First name must not be empty, Last name must not
be empty, Age must not be negative))
Problems with Take 2: Either
● Now, if validation errors exist, we get a list of all the errors!
● However, it was painful to achieve that
Solution: Validation
sealed trait Validation[+E, +A]
object Validation {
final case class Failure[+E](errors: NonEmptyChunk[E]) extends Validation[E, Nothing]
final case class Success[+A](value: A) extends Validation[Nothing, A]
}
Validation constructors
Validation.apply[A](a: => A): Validation[Throwable, A]
Validation.succeed[A](value: A): Validation[Nothing, A]
Validation.fail[E](error: E): Validation[E, Nothing]
Validation constructors
Validation.fromEither[E, A](value: Either[E, A]): Validation[E, A]
Validation.fromOption[A](value: Option[A]): Validation[Unit, A]
Validation.fromTry[A](value: => Try[A]): Validation[Throwable, A]
Validation.fromAssert[A](value: A)(assertion: Assertion[A]): Validation[String, A]
Validation constructors
Validation.mapParN[E, A0, A1,...,A21, B](
a0: Validation[E, A0],
a1: Validation[E, A1],
...,
a21: Validation[E, A21]
)(f: (A0, A1, ..., A21) => B): Validation[E, B]
Validation.tupledPar[E, A0, A1,..., A21](
a0: Validation[E, A0],
a1: Validation[E, A1],
...,
a21: Validation[E, A21]
): Validation[E, (A0, A1, ..., A21)]
Validation.collectAllPar[E, A](validations: Iterable[Validation[E, A]]): Validation[E, List[A]]
Validation operators
sealed trait Validation[+E, +A] {
def fold[B](failure: NonEmptyChunk[E] => B, success: A => B): B
def map[B](f: A => B): Validation[E, B]
def mapError[E1](f: E => E1): Validation[E1, A]
def flatMap[E1 >: E, B](f: A => Validation[E1, B]): Validation[E1, B]
def foreach[F[+_]: IdentityBoth: Covariant, B](f: A => F[B]): F[Validation[E, B]]
def zipPar[E1 >: E, B](that: Validation[E1, B]): Validation[E1, (A, B)]
def zipWithPar[E1 >: E, B, C](that: Validation[E1, B])(f: (A, B) => C): Validation[E1, C]
def zipParLeft[E1 >: E, B](that: Validation[E1, B]): Validation[E1, A]
def zipParRight[E1 >: E, B](that: Validation[E1, B]): Validation[E1, B]
def toEither[E1 >: E]: Either[NonEmptyChunk[E1], A]
def toOption: Option[A]
def toTry(implicit ev: E <:< Throwable): scala.util.Try[A]
def toZIO: IO[NonEmptyChunk[E], A]
}
Take 3: Validation
import zio.prelude._
sealed abstract case class Person private (firstName: String, lastName: String, age: Int)
object Person {
def make(firstName: String, lastName: String, age: Int): Validation[String, Person] =
Validation.mapParN(
validateFirstName(firstName),
validateLastName(lastName),
validateAge(age)
)((firstName, lastName, age) => new Person(firstName, lastName, age) {})
private def validateFirstName(firstName: String): Validation[String, String] =
if (firstName.nonEmpty) Validation.succeed(firstName)
else Validation.fail("First name must not be empty")
private def validateLastName(lastName: String): Validation[String, String] =
if (lastName.nonEmpty) Validation.succeed(lastName)
else Validation.fail("Last name must not be empty")
private def validateAge(age: Int): Validation[String, Int] =
if (age >= 0) Validation.succeed(age)
else Validation.fail("Age must not be negative")
}
Take 3: Validation
val validPerson = Person.make("Clark", "Kent", 30)
println(s"Valid Person: $validPerson")
// Valid Person: Success(Person(Clark,Kent,30))
val invalidPerson = Person.make("", "", -1)
println(s"Invalid Person: $invalidPerson")
// Invalid Person: Failure(NonEmptyChunk(First name must not be empty, Last
name must not be empty, Age must not be negative))
Validation benefits
● No implicits!
● Concrete methods
● Automatic error accumulation in NonEmptyChunk
Scenario 2:
Combining data structures
Example
Combine these two nested maps, summing stats:
val counters1: Map[String, Map[String, Stats]] = Map(
"Bolivia" -> Map(
"La Paz" -> Stats.make(None, Some(20), Some(30)),
"Cochabamba" -> Stats.make(Some(10), None, None),
"Santa Cruz" -> Stats.make(Some(10), Some(20), Some(30))
),
"United States" -> Map(
"California" -> Stats.make(None, None, Some(30)),
"New York" -> Stats.make(Some(10), Some(20), None),
"Texas" -> Stats.make(Some(10), Some(20), Some(30))
),
"Poland" -> Map(
"Pomerania" -> Stats.make(Some(10), Some(20), Some(30)),
"Masovia" -> Stats.make(Some(10), Some(20), Some(30)),
"Lublin" -> Stats.make(Some(10), Some(20), Some(30))
)
)
val counters2 = Map(
"Bolivia" -> Map(
"La Paz" -> Stats.make(Some(10), Some(20), Some(30)),
"Chuquisaca" -> Stats.make(Some(10), Some(20), Some(30))
),
"United States" -> Map(
"Alabama" -> Stats.make(None, Some(20), Some(70)),
"New York" -> Stats.make(Some(10), None, Some(30)),
"Florida" -> Stats.make(Some(50), Some(20), None),
"Nevada" -> Stats.make(Some(50), Some(20), Some(100))
),
"Poland" -> Map(
"West Pomerania" -> Stats.make(Some(10), Some(20), None),
"Silesia" -> Stats.make(Some(10), None, Some(30)),
"Lublin" -> Stats.make(None, Some(20), Some(30))
)
)
Take 1
final case class Stats(counter1: Option[Long], counter2: Option[Long], counter3: Option[Long]) { self =>
def combine(that: Stats): Stats =
Stats(
self.counter1.flatMap(c => that.counter1.map(c + _)),
self.counter2.flatMap(c => that.counter2.map(c + _)),
self.counter3.flatMap(c => that.counter3.map(c + _))
)
}
object Stats {
def make(counter1: Option[Long], counter2: Option[Long], counter3: Option[Long]): Stats =
Stats(counter1, counter2, counter3)
}
Take 1
def combine(left: Map[String, Map[String, Stats]], right: Map[String, Map[String, Stats]]): Map[String, Map[String, Stats]] = {
(left.keySet ++ right.keySet).map { country =>
val newValue = (left.get(country), right.get(country)) match {
case (Some(v1), None) => v1
case (None, Some(v2)) => v2
case (Some(v1), Some(v2)) =>
(v1.keySet ++ v2.keySet).map { region =>
val newStats = (v1.get(region), v2.get(region)) 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")
}
region -> newStats
}.toMap
case (None, None) => throw new Error("Unexpected scenario")
}
country -> newValue
}
}.toMap
Take 1
pprint.pprintln(combine(counters1, counters2))
/*
Map(
"Bolivia" -> Map(
"La Paz" -> Stats(None, Some(40L), Some(60L)),
"Cochabamba" -> Stats(Some(10L), None, None),
"Santa Cruz" -> Stats(Some(10L), Some(20L), Some(30L)),
"Chuquisaca" -> Stats(Some(10L), Some(20L), Some(30L))
),
"United States" -> Map(
"California" -> Stats(None, None, Some(30L)),
"Nevada" -> Stats(Some(50L), Some(20L), Some(100L)),
"Florida" -> Stats(Some(50L), Some(20L), None),
"Texas" -> Stats(Some(10L), Some(20L), Some(30L)),
"Alabama" -> Stats(None, Some(20L), Some(70L)),
"New York" -> Stats(Some(20L), None, None)
),
"Poland" -> Map(
"Silesia" -> Stats(Some(10L), None, Some(30L)),
"Lublin" -> Stats(None, Some(40L), Some(60L)),
"Masovia" -> Stats(Some(10L), Some(20L), Some(30L)),
"West Pomerania" -> Stats(Some(10L), Some(20L), None),
"Pomerania" -> Stats(Some(10L), Some(20L), Some(30L))
)
)
*/
val counters1: Map[String, Map[String, Stats]] = Map(
"Bolivia" -> Map(
"La Paz" -> Stats.make(None, Some(20), Some(30)),
"Cochabamba" -> Stats.make(Some(10), None, None),
"Santa Cruz" -> Stats.make(Some(10), Some(20), Some(30))
),
"United States" -> Map(
"California" -> Stats.make(None, None, Some(30)),
"New York" -> Stats.make(Some(10), Some(20), None),
"Texas" -> Stats.make(Some(10), Some(20), Some(30))
),
"Poland" -> Map(
"Pomerania" -> Stats.make(Some(10), Some(20), Some(30)),
"Masovia" -> Stats.make(Some(10), Some(20), Some(30)),
"Lublin" -> Stats.make(Some(10), Some(20), Some(30))
)
)
val counters2 = Map(
"Bolivia" -> Map(
"La Paz" -> Stats.make(Some(10), Some(20), Some(30)),
"Chuquisaca" -> Stats.make(Some(10), Some(20), Some(30))
),
"United States" -> Map(
"Alabama" -> Stats.make(None, Some(20), Some(70)),
"New York" -> Stats.make(Some(10), None, Some(30)),
"Florida" -> Stats.make(Some(50), Some(20), None),
"Nevada" -> Stats.make(Some(50), Some(20), Some(100))
),
"Poland" -> Map(
"West Pomerania" -> Stats.make(Some(10), Some(20), None),
"Silesia" -> Stats.make(Some(10), None, Some(30)),
"Lublin" -> Stats.make(None, Some(20), Some(30))
)
)
Problems with Take 1
● Buggy implementation!
● It was really painful to implement
Take 2
final case class Stats(counter1: Option[Long], counter2: Option[Long], counter3: Option[Long]) { self =>
def combine(that: Stats): Stats = {
def combineCounters(left: Option[Long], right: Option[Long]): Option[Long] =
(left, right) match {
case (Some(l), Some(r)) => Some(l + r)
case (Some(l), None) => Some(l)
case (None, Some(r)) => Some(r)
case (None, None) => None
}
Stats(
combineCounters(self.counter1, that.counter1),
combineCounters(self.counter2, that.counter2),
combineCounters(self.counter3, that.counter3)
)
}
}
object Stats {
def make(counter1: Option[Long], counter2: Option[Long], counter3: Option[Long]): Stats =
Stats(counter1, counter2, counter3)
}
Take 2
def combine(left: Map[String, Map[String, Stats]], right: Map[String, Map[String, Stats]]): Map[String, Map[String, Stats]] = {
(left.keySet ++ right.keySet).map { country =>
val newValue = (left.get(country), right.get(country)) match {
case (Some(v1), None) => v1
case (None, Some(v2)) => v2
case (Some(v1), Some(v2)) =>
(v1.keySet ++ v2.keySet).map { region =>
val newStats = (v1.get(region), v2.get(region)) 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")
}
region -> newStats
}.toMap
case (None, None) => throw new Error("Unexpected scenario")
}
country -> newValue
}
}.toMap
Take 2
val counters1: Map[String, Map[String, Stats]] = Map(
"Bolivia" -> Map(
"La Paz" -> Stats.make(None, Some(20), Some(30)),
"Cochabamba" -> Stats.make(Some(10), None, None),
"Santa Cruz" -> Stats.make(Some(10), Some(20), Some(30))
),
"United States" -> Map(
"California" -> Stats.make(None, None, Some(30)),
"New York" -> Stats.make(Some(10), Some(20), None),
"Texas" -> Stats.make(Some(10), Some(20), Some(30))
),
"Poland" -> Map(
"Pomerania" -> Stats.make(Some(10), Some(20), Some(30)),
"Masovia" -> Stats.make(Some(10), Some(20), Some(30)),
"Lublin" -> Stats.make(Some(10), Some(20), Some(30))
)
)
val counters2 = Map(
"Bolivia" -> Map(
"La Paz" -> Stats.make(Some(10), Some(20), Some(30)),
"Chuquisaca" -> Stats.make(Some(10), Some(20), Some(30))
),
"United States" -> Map(
"Alabama" -> Stats.make(None, Some(20), Some(70)),
"New York" -> Stats.make(Some(10), None, Some(30)),
"Florida" -> Stats.make(Some(50), Some(20), None),
"Nevada" -> Stats.make(Some(50), Some(20), Some(100))
),
"Poland" -> Map(
"West Pomerania" -> Stats.make(Some(10), Some(20), None),
"Silesia" -> Stats.make(Some(10), None, Some(30)),
"Lublin" -> Stats.make(None, Some(20), Some(30))
)
)
pprint.pprintln(combine(counters1, counters2))
/*
Map(
"Bolivia" -> Map(
"La Paz" -> Stats(Some(10L), Some(40L), Some(60L)),
"Cochabamba" -> Stats(Some(10L), None, None),
"Santa Cruz" -> Stats(Some(10L), Some(20L), Some(30L)),
"Chuquisaca" -> Stats(Some(10L), Some(20L), Some(30L))
),
"United States" -> Map(
"California" -> Stats(None, None, Some(30L)),
"Nevada" -> Stats(Some(50L), Some(20L), Some(100L)),
"Florida" -> Stats(Some(50L), Some(20L), None),
"Texas" -> Stats(Some(10L), Some(20L), Some(30L)),
"Alabama" -> Stats(None, Some(20L), Some(70L)),
"New York" -> Stats(Some(20L), Some(20L), Some(30L))
),
"Poland" -> Map(
"Silesia" -> Stats(Some(10L), None, Some(30L)),
"Lublin" -> Stats(Some(10L), Some(40L), Some(60L)),
"Masovia" -> Stats(Some(10L), Some(20L), Some(30L)),
"West Pomerania" -> Stats(Some(10L), Some(20L), None),
"Pomerania" -> Stats(Some(10L), Some(20L), Some(30L))
)
)
*/
Problems with Take 2
● It was really painful to implement
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!
Take 3
import zio.prelude._
final case class Stats(counter1: Option[Sum[Long]], counter2: Option[Sum[Long]], counter3: Option[Sum[Long]])
object Stats {
def make(counter1: Option[Long], counter2: Option[Long], counter3: Option[Long]): Stats =
Stats(counter1.map(Sum(_)), counter2.map(Sum(_)), counter3.map(Sum(_)))
implicit val associative: Associative[Stats] = new Associative[Stats] {
def combine(l: => Stats, r: => Stats): Stats =
Stats(l.counter1 <> r.counter1, l.counter2 <> r.counter2, l.counter3 <> r.counter3)
}
}
Take 3
val counters1: Map[String, Map[String, Stats]] = Map(
"Bolivia" -> Map(
"La Paz" -> Stats.make(None, Some(20), Some(30)),
"Cochabamba" -> Stats.make(Some(10), None, None),
"Santa Cruz" -> Stats.make(Some(10), Some(20), Some(30))
),
"United States" -> Map(
"California" -> Stats.make(None, None, Some(30)),
"New York" -> Stats.make(Some(10), Some(20), None),
"Texas" -> Stats.make(Some(10), Some(20), Some(30))
),
"Poland" -> Map(
"Pomerania" -> Stats.make(Some(10), Some(20), Some(30)),
"Masovia" -> Stats.make(Some(10), Some(20), Some(30)),
"Lublin" -> Stats.make(Some(10), Some(20), Some(30))
)
)
val counters2 = Map(
"Bolivia" -> Map(
"La Paz" -> Stats.make(Some(10), Some(20), Some(30)),
"Chuquisaca" -> Stats.make(Some(10), Some(20), Some(30))
),
"United States" -> Map(
"Alabama" -> Stats.make(None, Some(20), Some(70)),
"New York" -> Stats.make(Some(10), None, Some(30)),
"Florida" -> Stats.make(Some(50), Some(20), None),
"Nevada" -> Stats.make(Some(50), Some(20), Some(100))
),
"Poland" -> Map(
"West Pomerania" -> Stats.make(Some(10), Some(20), None),
"Silesia" -> Stats.make(Some(10), None, Some(30)),
"Lublin" -> Stats.make(None, Some(20), Some(30))
)
)
pprint.pprintln(counters1 <> counters2)
/*
Map(
"Bolivia" -> Map(
"La Paz" -> Stats(Some(10L), Some(40L), Some(60L)),
"Cochabamba" -> Stats(Some(10L), None, None),
"Santa Cruz" -> Stats(Some(10L), Some(20L), Some(30L)),
"Chuquisaca" -> Stats(Some(10L), Some(20L), Some(30L))
),
"United States" -> Map(
"California" -> Stats(None, None, Some(30L)),
"Nevada" -> Stats(Some(50L), Some(20L), Some(100L)),
"Florida" -> Stats(Some(50L), Some(20L), None),
"Texas" -> Stats(Some(10L), Some(20L), Some(30L)),
"Alabama" -> Stats(None, Some(20L), Some(70L)),
"New York" -> Stats(Some(20L), Some(20L), Some(30L))
),
"Poland" -> Map(
"Silesia" -> Stats(Some(10L), None, Some(30L)),
"Lublin" -> Stats(Some(10L), Some(40L), Some(60L)),
"Masovia" -> Stats(Some(10L), Some(20L), Some(30L)),
"West Pomerania" -> Stats(Some(10L), Some(20L), None),
"Pomerania" -> Stats(Some(10L), Some(20L), Some(30L))
)
)
*/
Associative benefits
● Does not lose information
● Newtypes to select binary operator
Scenario 3:
Traversing data
structures
Example 1
Process lists of raw events, where each event consists of a timestamp and
a description, and return either a List of valid events or a List of errors
found while processing the events.
Take 1
sealed abstract case class Event(timestamp: Long, description: String)
object Event {
def make(timestamp: Long, description: String): Either[String, Event] =
for {
validTimestamp <- validateTimestamp(timestamp)
validDescription <- validateDescription(description)
} yield new Event(validTimestamp, validDescription) {}
private def validateTimestamp(timestamp: Long): Either[String, Long] =
if (timestamp >= 0) Right(timestamp)
else Left(s"Timestamp must not be negative: $timestamp")
private def validateDescription(description: String): Either[String, String] =
if (description.nonEmpty) Right(description)
else Left("Description must not be empty")
}
def collectEvents(rawEvents: List[(Int, String)]): Either[String, List[Event]] =
rawEvents.foldRight(Right(List.empty): Either[String, List[Event]]) {
case ((timestamp, description), events) =>
Event.make(timestamp, description).flatMap(event => events.map(event +: _))
}
Take 1
def main(args: Array[String]): Unit = {
val rawEvents: List[(Int, String)] = List(
(1000, "Event 1"),
(2000, "Event 2"),
(3000, "Event 3"),
(4000, "Event 4"),
(5000, "Event 5")
)
val rawEvents2: List[(Int, String)] = List(
(1000, "Event 1"),
(-2, ""),
(3000, ""),
(4000, "Event 4"),
(-5, "Event 5")
)
val validatedEvents = collectEvents(rawEvents)
val validatedEvents2 = collectEvents(rawEvents2)
println(validatedEvents)
// Right(List(Event(1000,Event 1), Event(2000,Event 2), Event(3000,Event 3), Event(4000,Event 4), Event(5000,Event 5)))
println(validatedEvents2)
// Left(Timestamp must not be negative: -2)
}
Problems with Take 1
● Fail-fast implementation!
● It was a little difficult to implement collectEvents
Take 2
sealed abstract case class Event(timestamp: Long, description: String)
object Event {
def make(timestamp: Long, description: String): Validation[String, Event] =
Validation.mapParN(validateTimestamp(timestamp), validateDescription(description)) {
case (validTimestamp, validDescription) => new Event(validTimestamp, validDescription) {}
}
private def validateTimestamp(timestamp: Long): Validation[String, Long] =
if (timestamp >= 0) Validation.succeed(timestamp)
else Validation.fail(s"Timestamp must not be negative: $timestamp")
private def validateDescription(description: String): Validation[String, String] =
if (description.nonEmpty) Validation.succeed(description)
else Validation.fail("Description must not be empty")
}
def collectEvents(rawEvents: List[(Int, String)]): Validation[String, List[Event]] =
rawEvents.foldRight(Validation.succeed(List.empty): Validation[String, List[Event]]) {
case ((timestamp, description), events) =>
Event.make(timestamp, description).zipWith(events) { (event, events) =>
event +: events
}
}
Take 2
def main(args: Array[String]): Unit = {
val rawEvents: List[(Int, String)] = List(
(1000, "Event 1"),
(2000, "Event 2"),
(3000, "Event 3"),
(4000, "Event 4"),
(5000, "Event 5")
)
val rawEvents2: List[(Int, String)] = List(
(1000, "Event 1"),
(-2, ""),
(3000, ""),
(4000, "Event 4"),
(-5, "Event 5")
)
val validatedEvents = collectEvents(rawEvents)
val validatedEvents2 = collectEvents(rawEvents2)
println(validatedEvents)
// Success(List(Event(1000,Event 1), Event(2000,Event 2), Event(3000,Event 3), Event(4000,Event 4), Event(5000,Event 5)))
println(validatedEvents2)
// Failure(NonEmptyChunk(Timestamp must not be negative: -2, Description must not be empty, Description must not be empty, Timestamp
must not be negative: -5))
}
Problems with Take 2
● It was a little difficult to implement collectEvents
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
ZIO Prelude has Traversable instances for Scala Standard Types:
● Option
● Either
● List
● Vector
● Map
Solution: Traversable Typeclass
And, of course, we can create instances of Traversable for our
own collection types!
Take 3
def main(args: Array[String]): Unit = {
val rawEvents: List[(Int, String)] = List(
(1000, "Event 1"),
(2000, "Event 2"),
(3000, "Event 3"),
(4000, "Event 4"),
(5000, "Event 5")
)
val rawEvents2: List[(Int, String)] = List(
(1000, "Event 1"),
(-2, ""),
(3000, ""),
(4000, "Event 4"),
(-5, "Event 5")
)
val validatedEvents = Traversable[List].foreach(rawEvents) {
case (timestamp, description) => Event.make(timestamp, description)
}
val validatedEvents2 = Traversable[List].foreach(rawEvents2) {
case (timestamp, description) => Event.make(timestamp, description)
}
println(validatedEvents)
// Success(List(Event(1000,Event 1), Event(2000,Event 2), Event(3000,Event 3), Event(4000,Event 4), Event(5000,Event 5)))
println(validatedEvents2)
// Failure(NonEmptyChunk(Timestamp must not be negative: -2, Description must not be empty, Description must not be empty, Timestamp must not be negative: -5))
}
Example 2
Traversing a binary tree of Futures
Solution
import zio.prelude._
sealed trait BinaryTree[+A] { self =>
def map[B](f: A => B): BinaryTree[B] = self match {
case BinaryTree.Leaf(a) => BinaryTree.Leaf(f(a))
case BinaryTree.Branch(left, right) => BinaryTree.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: Traversable[BinaryTree] = 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(_, _))
}
}
}
Solution
implicit val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(5))
def echo(message: String): Future[String] = Future {
Thread.sleep(1000)
s"Echo: $message"
}
def main(args: Array[String]): Unit = {
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"))
)
)
*/
}
Traversable benefits
● Works on all Scala collection types
● Just by implementing foreach, we get a lot of additional
methods for free!
Scenario 4:
Executing Pure
Computations
Example
Write a program that, given an user ID, looks for the user's name and age, and then:
● If user’s name is Jorge: The program should fail with an error message saying that you
can't ask for Jorge's age.
● Otherwise:
○ If user's age is less than 50: The program should succeed with a log message
including name and age
○ Otherwise: The program should succeed with no value
Take 1: Monad Transformers
val getUserName: Reader[Long, String] = Reader { id =>
if (1 <= id && id < 100) "Jorge"
else if (100 <= id && id < 200) "Anne"
else "Claire"
}
val getUserAge: Reader[Long, Int] = Reader { id =>
if (1 <= id && id < 100) 30
else if (100 <= id && id < 200) 50
else 20
}
def log(msg: String): String = s"Log: $msg"
val program: OptionT[EitherT[Reader[Long, *], String, *], String] =
for {
userName <- OptionT.liftF[EitherT[Reader[Long, *], String, *], String](EitherT.liftF[Reader[Long, *], String, String](getUserName))
userAge <- OptionT.liftF[EitherT[Reader[Long, *], String, *], Int](EitherT.liftF[Reader[Long, *], String, Int](getUserAge))
logMessage <- if (userName == "Jorge")
OptionT.liftF[EitherT[Reader[Long, *], String, *], String](
EitherT.leftT[Reader[Long, *], String]("You are not allowed to know Jorge's age!")
)
else if (userAge < 50)
OptionT.some[EitherT[Reader[Long, *], String, *]](s"Name: $userName, Age: $userAge")
else
OptionT.none[EitherT[Reader[Long, *], String, *], String]
log <- OptionT.liftF[EitherT[Reader[Long, *], String, *], String](EitherT.rightT[Reader[Long, *], String](log(logMessage)))
} yield log
def runProgram(id: Long): Either[String, Option[String]] = program.value.value.run(id)
Take 1: Monad Transformers
println(runProgram(10)) // Left(You are not allowed to know Jorge's age!)
println(runProgram(150)) // Right(None)
println(runProgram(250)) // Right(Some(Log: Name: Claire, Age: 20))
Problems with Take 1: Monad Transformers
● Complicated
● Types are not inferred (at all)
● Slow
Solution: ZPure
ZPure[-S1, +S2, -R, +E, +A]
Solution: ZPure
ZPure[S1, S2, R, E, A] is a purely functional description
of a computation that requires an environment R and an initial
state S1 and may either fail with an E or succeed with an
updated state S2 and an A. Because of its polymorphism
ZPure can be used to model a variety of effects including
context, state, and failure.
ZPure Mental Model
R => S1 => (S2, Either[E, A])
ZPure type aliases
type EState[S, +E, +A] = ZPure[S, S, Any, E, A]
type State[S, +A] = ZPure[S, S, Any, Nothing, A]
type Reader[-R, +A] = ZPure[Unit, Unit, R, Nothing, A]
type EReader[-R, +E, +A] = ZPure[Unit, Unit, R, E, A]
Take 2: ZPure
import zio.prelude._
val getUserName: Reader[Long, String] = Reader.fromFunction { id =>
if (1 <= id && id < 100) "Jorge"
else if (100 <= id && id < 200) "Anne"
else "Claire"
}
val getUserAge: Reader[Long, Int] = Reader.fromFunction { id =>
if (1 <= id && id < 100) 30
else if (100 <= id && id < 200) 50
else 20
}
def log(msg: String): String = s"Log: $msg"
val program: EReader[Long, String, Option[String]] =
for {
userName <- getUserName
userAge <- getUserAge
logMessage <- if (userName == "Jorge")
ZPure.fail("You are not allowed to know Jorge's age!")
else if (userAge < 50)
ZPure.succeed[Unit, String](s"Name: $userName, Age: $userAge").asSome
else
ZPure.succeed[Unit, Option[String]](None)
} yield logMessage.map(log)
def runProgram(id: Long): Either[String, Option[String]] = program.provide(id).runEither(()).map(_._2)
Take 2: ZPure
println(runProgram(10)) // Left(You are not allowed to know Jorge's age!)
println(runProgram(150)) // Right(None)
println(runProgram(250)) // Right(Some(Log: Name: Claire, Age: 20))
ZPure benefits
● Simple
● Great type inference
● Fast!
Special thanks
● To Scalac Functional World for hosting this presentation
● To John De Goes for guidance and technical checking
Thank You!
Where to learn more
● SF Scala: Reimagining Functional Type Classes, talk by
John De Goes and Adam Fraser
@jorvasquez2301
jorge.vasquez@scalac.io
jorge-vasquez-2301
Contact me
Questions?

Exploring ZIO Prelude: The game changer for typeclasses in Scala

  • 1.
    Exploring ZIO Prelude: TheGame-Changer for Typeclasses in Scala
  • 2.
  • 4.
    Agenda ● Motivations aroundZIO Prelude ● Tour of ZIO Prelude ○ Validating data ○ Combining data structures ○ Traversing data structures ○ Executing pure computations
  • 5.
  • 6.
    Background ● Functional Scalawas born in Haskell. ● Scalaz took proven machinery in Haskell, and ported it to Scala. ● Scalaz has been forked and replicated into other libraries.
  • 7.
    But Scala isnot Haskell! Scala combines object-oriented and functional programming in one concise, high-level language.
  • 8.
    But Scala isnot Haskell! Scala does not have everything Haskell has: ● Lack of full type inference ● Adds more performance overhead
  • 9.
    But Scala isnot Haskell! Scala has things Haskell does not have! ● Powerful subtyping ● Powerful declaration-site variance
  • 10.
    In conclusion... We needa Scala-first take on functional abstractions: ZIO Prelude gives us that!
  • 11.
    ZIO Prelude givesus... ● Modular hierarchy that relies on subtyping ● Declaration-site variance ● Great type inference
  • 12.
    ZIO Prelude givesus... ● No Haskell-isms! ○ ap on Applicative ○ Over reliance on Unit ○ Invariance
  • 13.
    ZIO Prelude givesus... ● Minimal implicits ● Less indirection
  • 14.
    Tour of ZIOPrelude
  • 15.
    ZIO Prelude ● ZIOPrelude is a small library ● Today we will cover a smaller part
  • 16.
    ZIO Prelude onGitHub https://github.com/zio/zio-prelude/
  • 17.
  • 18.
    Example Validate Person data: ●First name must not be empty ● Last name must not be empty ● Age must not be negative
  • 19.
    Take 1: Either sealedabstract case class Person private (firstName: String, lastName: String, age: Int) object Person { def make(firstName: String, lastName: String, age: Int): Either[String, Person] = for { validFirstName <- validateFirstName(firstName) validLastName <- validateLastName(lastName) validAge <- validateAge(age) } yield new Person(validFirstName, validLastName, validAge) {} private def validateFirstName(firstName: String): Either[String, String] = if (firstName.nonEmpty) Right(firstName) else Left("First name must not be empty") private def validateLastName(lastName: String): Either[String, String] = if (lastName.nonEmpty) Right(lastName) else Left("Last name must not be empty") private def validateAge(age: Int): Either[String, Int] = if (age >= 0) Right(age) else Left("Age must not be negative") }
  • 20.
    Take 1: Either valvalidPerson = Person.make("Clark", "Kent", 30) println(s"Valid Person: $validPerson") // Valid Person: Right(Person(Clark,Kent,30)) val invalidPerson = Person.make("", "", -1) println(s"Invalid Person: $invalidPerson") // Invalid Person: Left(First name must not be empty)
  • 21.
    Problems with Take1: Either ● Short-circuiting on failure ● If validation errors exist, we just get the first one!
  • 22.
    Take 2: Either defmake(firstName: String, lastName: String, age: Int): Either[List[String], Person] = validateFirstName(firstName) match { case Left(errorFirstName) => validateLastName(lastName) match { case Left(errorLastName) => validateAge(age) match { case Left(errorAge) => Left(List(errorFirstName, errorLastName, errorAge)) case Right(_) => Left(List(errorFirstName, errorLastName)) } case Right(_) => validateAge(age) match { case Left(error2) => Left(List(errorFirstName, error2)) case Right(_) => Left(List(errorFirstName)) } } case Right(firstName) => validateLastName(lastName) match { case Left(errorLastName) => validateAge(age) match { case Left(errorAge) => Left(List(errorLastName, errorAge)) case Right(_) => Left(List(errorLastName)) } case Right(lastName) => validateAge(age) match { case Left(errorAge) => Left(List(errorAge)) case Right(age) => Right(new Person(firstName, lastName, age) {}) } } }
  • 23.
    Take 2: Either valvalidPerson = Person.make("Clark", "Kent", 30) println(s"Valid Person: $validPerson") // Valid Person: Right(Person(Clark,Kent,30)) val invalidPerson = Person.make("", "", -1) println(s"Invalid Person: $invalidPerson") // Invalid Person: Left(List(First name must not be empty, Last name must not be empty, Age must not be negative))
  • 24.
    Problems with Take2: Either ● Now, if validation errors exist, we get a list of all the errors! ● However, it was painful to achieve that
  • 25.
    Solution: Validation sealed traitValidation[+E, +A] object Validation { final case class Failure[+E](errors: NonEmptyChunk[E]) extends Validation[E, Nothing] final case class Success[+A](value: A) extends Validation[Nothing, A] }
  • 26.
    Validation constructors Validation.apply[A](a: =>A): Validation[Throwable, A] Validation.succeed[A](value: A): Validation[Nothing, A] Validation.fail[E](error: E): Validation[E, Nothing]
  • 27.
    Validation constructors Validation.fromEither[E, A](value:Either[E, A]): Validation[E, A] Validation.fromOption[A](value: Option[A]): Validation[Unit, A] Validation.fromTry[A](value: => Try[A]): Validation[Throwable, A] Validation.fromAssert[A](value: A)(assertion: Assertion[A]): Validation[String, A]
  • 28.
    Validation constructors Validation.mapParN[E, A0,A1,...,A21, B]( a0: Validation[E, A0], a1: Validation[E, A1], ..., a21: Validation[E, A21] )(f: (A0, A1, ..., A21) => B): Validation[E, B] Validation.tupledPar[E, A0, A1,..., A21]( a0: Validation[E, A0], a1: Validation[E, A1], ..., a21: Validation[E, A21] ): Validation[E, (A0, A1, ..., A21)] Validation.collectAllPar[E, A](validations: Iterable[Validation[E, A]]): Validation[E, List[A]]
  • 29.
    Validation operators sealed traitValidation[+E, +A] { def fold[B](failure: NonEmptyChunk[E] => B, success: A => B): B def map[B](f: A => B): Validation[E, B] def mapError[E1](f: E => E1): Validation[E1, A] def flatMap[E1 >: E, B](f: A => Validation[E1, B]): Validation[E1, B] def foreach[F[+_]: IdentityBoth: Covariant, B](f: A => F[B]): F[Validation[E, B]] def zipPar[E1 >: E, B](that: Validation[E1, B]): Validation[E1, (A, B)] def zipWithPar[E1 >: E, B, C](that: Validation[E1, B])(f: (A, B) => C): Validation[E1, C] def zipParLeft[E1 >: E, B](that: Validation[E1, B]): Validation[E1, A] def zipParRight[E1 >: E, B](that: Validation[E1, B]): Validation[E1, B] def toEither[E1 >: E]: Either[NonEmptyChunk[E1], A] def toOption: Option[A] def toTry(implicit ev: E <:< Throwable): scala.util.Try[A] def toZIO: IO[NonEmptyChunk[E], A] }
  • 30.
    Take 3: Validation importzio.prelude._ sealed abstract case class Person private (firstName: String, lastName: String, age: Int) object Person { def make(firstName: String, lastName: String, age: Int): Validation[String, Person] = Validation.mapParN( validateFirstName(firstName), validateLastName(lastName), validateAge(age) )((firstName, lastName, age) => new Person(firstName, lastName, age) {}) private def validateFirstName(firstName: String): Validation[String, String] = if (firstName.nonEmpty) Validation.succeed(firstName) else Validation.fail("First name must not be empty") private def validateLastName(lastName: String): Validation[String, String] = if (lastName.nonEmpty) Validation.succeed(lastName) else Validation.fail("Last name must not be empty") private def validateAge(age: Int): Validation[String, Int] = if (age >= 0) Validation.succeed(age) else Validation.fail("Age must not be negative") }
  • 31.
    Take 3: Validation valvalidPerson = Person.make("Clark", "Kent", 30) println(s"Valid Person: $validPerson") // Valid Person: Success(Person(Clark,Kent,30)) val invalidPerson = Person.make("", "", -1) println(s"Invalid Person: $invalidPerson") // Invalid Person: Failure(NonEmptyChunk(First name must not be empty, Last name must not be empty, Age must not be negative))
  • 32.
    Validation benefits ● Noimplicits! ● Concrete methods ● Automatic error accumulation in NonEmptyChunk
  • 33.
  • 34.
    Example Combine these twonested maps, summing stats: val counters1: Map[String, Map[String, Stats]] = Map( "Bolivia" -> Map( "La Paz" -> Stats.make(None, Some(20), Some(30)), "Cochabamba" -> Stats.make(Some(10), None, None), "Santa Cruz" -> Stats.make(Some(10), Some(20), Some(30)) ), "United States" -> Map( "California" -> Stats.make(None, None, Some(30)), "New York" -> Stats.make(Some(10), Some(20), None), "Texas" -> Stats.make(Some(10), Some(20), Some(30)) ), "Poland" -> Map( "Pomerania" -> Stats.make(Some(10), Some(20), Some(30)), "Masovia" -> Stats.make(Some(10), Some(20), Some(30)), "Lublin" -> Stats.make(Some(10), Some(20), Some(30)) ) ) val counters2 = Map( "Bolivia" -> Map( "La Paz" -> Stats.make(Some(10), Some(20), Some(30)), "Chuquisaca" -> Stats.make(Some(10), Some(20), Some(30)) ), "United States" -> Map( "Alabama" -> Stats.make(None, Some(20), Some(70)), "New York" -> Stats.make(Some(10), None, Some(30)), "Florida" -> Stats.make(Some(50), Some(20), None), "Nevada" -> Stats.make(Some(50), Some(20), Some(100)) ), "Poland" -> Map( "West Pomerania" -> Stats.make(Some(10), Some(20), None), "Silesia" -> Stats.make(Some(10), None, Some(30)), "Lublin" -> Stats.make(None, Some(20), Some(30)) ) )
  • 35.
    Take 1 final caseclass Stats(counter1: Option[Long], counter2: Option[Long], counter3: Option[Long]) { self => def combine(that: Stats): Stats = Stats( self.counter1.flatMap(c => that.counter1.map(c + _)), self.counter2.flatMap(c => that.counter2.map(c + _)), self.counter3.flatMap(c => that.counter3.map(c + _)) ) } object Stats { def make(counter1: Option[Long], counter2: Option[Long], counter3: Option[Long]): Stats = Stats(counter1, counter2, counter3) }
  • 36.
    Take 1 def combine(left:Map[String, Map[String, Stats]], right: Map[String, Map[String, Stats]]): Map[String, Map[String, Stats]] = { (left.keySet ++ right.keySet).map { country => val newValue = (left.get(country), right.get(country)) match { case (Some(v1), None) => v1 case (None, Some(v2)) => v2 case (Some(v1), Some(v2)) => (v1.keySet ++ v2.keySet).map { region => val newStats = (v1.get(region), v2.get(region)) 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") } region -> newStats }.toMap case (None, None) => throw new Error("Unexpected scenario") } country -> newValue } }.toMap
  • 37.
    Take 1 pprint.pprintln(combine(counters1, counters2)) /* Map( "Bolivia"-> Map( "La Paz" -> Stats(None, Some(40L), Some(60L)), "Cochabamba" -> Stats(Some(10L), None, None), "Santa Cruz" -> Stats(Some(10L), Some(20L), Some(30L)), "Chuquisaca" -> Stats(Some(10L), Some(20L), Some(30L)) ), "United States" -> Map( "California" -> Stats(None, None, Some(30L)), "Nevada" -> Stats(Some(50L), Some(20L), Some(100L)), "Florida" -> Stats(Some(50L), Some(20L), None), "Texas" -> Stats(Some(10L), Some(20L), Some(30L)), "Alabama" -> Stats(None, Some(20L), Some(70L)), "New York" -> Stats(Some(20L), None, None) ), "Poland" -> Map( "Silesia" -> Stats(Some(10L), None, Some(30L)), "Lublin" -> Stats(None, Some(40L), Some(60L)), "Masovia" -> Stats(Some(10L), Some(20L), Some(30L)), "West Pomerania" -> Stats(Some(10L), Some(20L), None), "Pomerania" -> Stats(Some(10L), Some(20L), Some(30L)) ) ) */ val counters1: Map[String, Map[String, Stats]] = Map( "Bolivia" -> Map( "La Paz" -> Stats.make(None, Some(20), Some(30)), "Cochabamba" -> Stats.make(Some(10), None, None), "Santa Cruz" -> Stats.make(Some(10), Some(20), Some(30)) ), "United States" -> Map( "California" -> Stats.make(None, None, Some(30)), "New York" -> Stats.make(Some(10), Some(20), None), "Texas" -> Stats.make(Some(10), Some(20), Some(30)) ), "Poland" -> Map( "Pomerania" -> Stats.make(Some(10), Some(20), Some(30)), "Masovia" -> Stats.make(Some(10), Some(20), Some(30)), "Lublin" -> Stats.make(Some(10), Some(20), Some(30)) ) ) val counters2 = Map( "Bolivia" -> Map( "La Paz" -> Stats.make(Some(10), Some(20), Some(30)), "Chuquisaca" -> Stats.make(Some(10), Some(20), Some(30)) ), "United States" -> Map( "Alabama" -> Stats.make(None, Some(20), Some(70)), "New York" -> Stats.make(Some(10), None, Some(30)), "Florida" -> Stats.make(Some(50), Some(20), None), "Nevada" -> Stats.make(Some(50), Some(20), Some(100)) ), "Poland" -> Map( "West Pomerania" -> Stats.make(Some(10), Some(20), None), "Silesia" -> Stats.make(Some(10), None, Some(30)), "Lublin" -> Stats.make(None, Some(20), Some(30)) ) )
  • 38.
    Problems with Take1 ● Buggy implementation! ● It was really painful to implement
  • 39.
    Take 2 final caseclass Stats(counter1: Option[Long], counter2: Option[Long], counter3: Option[Long]) { self => def combine(that: Stats): Stats = { def combineCounters(left: Option[Long], right: Option[Long]): Option[Long] = (left, right) match { case (Some(l), Some(r)) => Some(l + r) case (Some(l), None) => Some(l) case (None, Some(r)) => Some(r) case (None, None) => None } Stats( combineCounters(self.counter1, that.counter1), combineCounters(self.counter2, that.counter2), combineCounters(self.counter3, that.counter3) ) } } object Stats { def make(counter1: Option[Long], counter2: Option[Long], counter3: Option[Long]): Stats = Stats(counter1, counter2, counter3) }
  • 40.
    Take 2 def combine(left:Map[String, Map[String, Stats]], right: Map[String, Map[String, Stats]]): Map[String, Map[String, Stats]] = { (left.keySet ++ right.keySet).map { country => val newValue = (left.get(country), right.get(country)) match { case (Some(v1), None) => v1 case (None, Some(v2)) => v2 case (Some(v1), Some(v2)) => (v1.keySet ++ v2.keySet).map { region => val newStats = (v1.get(region), v2.get(region)) 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") } region -> newStats }.toMap case (None, None) => throw new Error("Unexpected scenario") } country -> newValue } }.toMap
  • 41.
    Take 2 val counters1:Map[String, Map[String, Stats]] = Map( "Bolivia" -> Map( "La Paz" -> Stats.make(None, Some(20), Some(30)), "Cochabamba" -> Stats.make(Some(10), None, None), "Santa Cruz" -> Stats.make(Some(10), Some(20), Some(30)) ), "United States" -> Map( "California" -> Stats.make(None, None, Some(30)), "New York" -> Stats.make(Some(10), Some(20), None), "Texas" -> Stats.make(Some(10), Some(20), Some(30)) ), "Poland" -> Map( "Pomerania" -> Stats.make(Some(10), Some(20), Some(30)), "Masovia" -> Stats.make(Some(10), Some(20), Some(30)), "Lublin" -> Stats.make(Some(10), Some(20), Some(30)) ) ) val counters2 = Map( "Bolivia" -> Map( "La Paz" -> Stats.make(Some(10), Some(20), Some(30)), "Chuquisaca" -> Stats.make(Some(10), Some(20), Some(30)) ), "United States" -> Map( "Alabama" -> Stats.make(None, Some(20), Some(70)), "New York" -> Stats.make(Some(10), None, Some(30)), "Florida" -> Stats.make(Some(50), Some(20), None), "Nevada" -> Stats.make(Some(50), Some(20), Some(100)) ), "Poland" -> Map( "West Pomerania" -> Stats.make(Some(10), Some(20), None), "Silesia" -> Stats.make(Some(10), None, Some(30)), "Lublin" -> Stats.make(None, Some(20), Some(30)) ) ) pprint.pprintln(combine(counters1, counters2)) /* Map( "Bolivia" -> Map( "La Paz" -> Stats(Some(10L), Some(40L), Some(60L)), "Cochabamba" -> Stats(Some(10L), None, None), "Santa Cruz" -> Stats(Some(10L), Some(20L), Some(30L)), "Chuquisaca" -> Stats(Some(10L), Some(20L), Some(30L)) ), "United States" -> Map( "California" -> Stats(None, None, Some(30L)), "Nevada" -> Stats(Some(50L), Some(20L), Some(100L)), "Florida" -> Stats(Some(50L), Some(20L), None), "Texas" -> Stats(Some(10L), Some(20L), Some(30L)), "Alabama" -> Stats(None, Some(20L), Some(70L)), "New York" -> Stats(Some(20L), Some(20L), Some(30L)) ), "Poland" -> Map( "Silesia" -> Stats(Some(10L), None, Some(30L)), "Lublin" -> Stats(Some(10L), Some(40L), Some(60L)), "Masovia" -> Stats(Some(10L), Some(20L), Some(30L)), "West Pomerania" -> Stats(Some(10L), Some(20L), None), "Pomerania" -> Stats(Some(10L), Some(20L), Some(30L)) ) ) */
  • 42.
    Problems with Take2 ● It was really painful to implement
  • 43.
    Solution: Associative Typeclass traitAssociative[A] { def combine(l: => A, r: => A): A } // Associativity law (a1 <> (a2 <> a3)) <-> ((a1 <> a2) <> a3)
  • 44.
    Solution: Associative Typeclass ZIOPrelude has Associativeinstances for Scala Standard Types: ● Boolean ● Byte ● Char ● Short ● Int ● Long ● Float ● Double ● String ● Option ● Vector ● List ● Set ● Tuple
  • 45.
    Solution: Associative Typeclass And,of course, we can create instances of Associative for our own types!
  • 46.
    Take 3 import zio.prelude._ finalcase class Stats(counter1: Option[Sum[Long]], counter2: Option[Sum[Long]], counter3: Option[Sum[Long]]) object Stats { def make(counter1: Option[Long], counter2: Option[Long], counter3: Option[Long]): Stats = Stats(counter1.map(Sum(_)), counter2.map(Sum(_)), counter3.map(Sum(_))) implicit val associative: Associative[Stats] = new Associative[Stats] { def combine(l: => Stats, r: => Stats): Stats = Stats(l.counter1 <> r.counter1, l.counter2 <> r.counter2, l.counter3 <> r.counter3) } }
  • 47.
    Take 3 val counters1:Map[String, Map[String, Stats]] = Map( "Bolivia" -> Map( "La Paz" -> Stats.make(None, Some(20), Some(30)), "Cochabamba" -> Stats.make(Some(10), None, None), "Santa Cruz" -> Stats.make(Some(10), Some(20), Some(30)) ), "United States" -> Map( "California" -> Stats.make(None, None, Some(30)), "New York" -> Stats.make(Some(10), Some(20), None), "Texas" -> Stats.make(Some(10), Some(20), Some(30)) ), "Poland" -> Map( "Pomerania" -> Stats.make(Some(10), Some(20), Some(30)), "Masovia" -> Stats.make(Some(10), Some(20), Some(30)), "Lublin" -> Stats.make(Some(10), Some(20), Some(30)) ) ) val counters2 = Map( "Bolivia" -> Map( "La Paz" -> Stats.make(Some(10), Some(20), Some(30)), "Chuquisaca" -> Stats.make(Some(10), Some(20), Some(30)) ), "United States" -> Map( "Alabama" -> Stats.make(None, Some(20), Some(70)), "New York" -> Stats.make(Some(10), None, Some(30)), "Florida" -> Stats.make(Some(50), Some(20), None), "Nevada" -> Stats.make(Some(50), Some(20), Some(100)) ), "Poland" -> Map( "West Pomerania" -> Stats.make(Some(10), Some(20), None), "Silesia" -> Stats.make(Some(10), None, Some(30)), "Lublin" -> Stats.make(None, Some(20), Some(30)) ) ) pprint.pprintln(counters1 <> counters2) /* Map( "Bolivia" -> Map( "La Paz" -> Stats(Some(10L), Some(40L), Some(60L)), "Cochabamba" -> Stats(Some(10L), None, None), "Santa Cruz" -> Stats(Some(10L), Some(20L), Some(30L)), "Chuquisaca" -> Stats(Some(10L), Some(20L), Some(30L)) ), "United States" -> Map( "California" -> Stats(None, None, Some(30L)), "Nevada" -> Stats(Some(50L), Some(20L), Some(100L)), "Florida" -> Stats(Some(50L), Some(20L), None), "Texas" -> Stats(Some(10L), Some(20L), Some(30L)), "Alabama" -> Stats(None, Some(20L), Some(70L)), "New York" -> Stats(Some(20L), Some(20L), Some(30L)) ), "Poland" -> Map( "Silesia" -> Stats(Some(10L), None, Some(30L)), "Lublin" -> Stats(Some(10L), Some(40L), Some(60L)), "Masovia" -> Stats(Some(10L), Some(20L), Some(30L)), "West Pomerania" -> Stats(Some(10L), Some(20L), None), "Pomerania" -> Stats(Some(10L), Some(20L), Some(30L)) ) ) */
  • 48.
    Associative benefits ● Doesnot lose information ● Newtypes to select binary operator
  • 49.
  • 50.
    Example 1 Process listsof raw events, where each event consists of a timestamp and a description, and return either a List of valid events or a List of errors found while processing the events.
  • 51.
    Take 1 sealed abstractcase class Event(timestamp: Long, description: String) object Event { def make(timestamp: Long, description: String): Either[String, Event] = for { validTimestamp <- validateTimestamp(timestamp) validDescription <- validateDescription(description) } yield new Event(validTimestamp, validDescription) {} private def validateTimestamp(timestamp: Long): Either[String, Long] = if (timestamp >= 0) Right(timestamp) else Left(s"Timestamp must not be negative: $timestamp") private def validateDescription(description: String): Either[String, String] = if (description.nonEmpty) Right(description) else Left("Description must not be empty") } def collectEvents(rawEvents: List[(Int, String)]): Either[String, List[Event]] = rawEvents.foldRight(Right(List.empty): Either[String, List[Event]]) { case ((timestamp, description), events) => Event.make(timestamp, description).flatMap(event => events.map(event +: _)) }
  • 52.
    Take 1 def main(args:Array[String]): Unit = { val rawEvents: List[(Int, String)] = List( (1000, "Event 1"), (2000, "Event 2"), (3000, "Event 3"), (4000, "Event 4"), (5000, "Event 5") ) val rawEvents2: List[(Int, String)] = List( (1000, "Event 1"), (-2, ""), (3000, ""), (4000, "Event 4"), (-5, "Event 5") ) val validatedEvents = collectEvents(rawEvents) val validatedEvents2 = collectEvents(rawEvents2) println(validatedEvents) // Right(List(Event(1000,Event 1), Event(2000,Event 2), Event(3000,Event 3), Event(4000,Event 4), Event(5000,Event 5))) println(validatedEvents2) // Left(Timestamp must not be negative: -2) }
  • 53.
    Problems with Take1 ● Fail-fast implementation! ● It was a little difficult to implement collectEvents
  • 54.
    Take 2 sealed abstractcase class Event(timestamp: Long, description: String) object Event { def make(timestamp: Long, description: String): Validation[String, Event] = Validation.mapParN(validateTimestamp(timestamp), validateDescription(description)) { case (validTimestamp, validDescription) => new Event(validTimestamp, validDescription) {} } private def validateTimestamp(timestamp: Long): Validation[String, Long] = if (timestamp >= 0) Validation.succeed(timestamp) else Validation.fail(s"Timestamp must not be negative: $timestamp") private def validateDescription(description: String): Validation[String, String] = if (description.nonEmpty) Validation.succeed(description) else Validation.fail("Description must not be empty") } def collectEvents(rawEvents: List[(Int, String)]): Validation[String, List[Event]] = rawEvents.foldRight(Validation.succeed(List.empty): Validation[String, List[Event]]) { case ((timestamp, description), events) => Event.make(timestamp, description).zipWith(events) { (event, events) => event +: events } }
  • 55.
    Take 2 def main(args:Array[String]): Unit = { val rawEvents: List[(Int, String)] = List( (1000, "Event 1"), (2000, "Event 2"), (3000, "Event 3"), (4000, "Event 4"), (5000, "Event 5") ) val rawEvents2: List[(Int, String)] = List( (1000, "Event 1"), (-2, ""), (3000, ""), (4000, "Event 4"), (-5, "Event 5") ) val validatedEvents = collectEvents(rawEvents) val validatedEvents2 = collectEvents(rawEvents2) println(validatedEvents) // Success(List(Event(1000,Event 1), Event(2000,Event 2), Event(3000,Event 3), Event(4000,Event 4), Event(5000,Event 5))) println(validatedEvents2) // Failure(NonEmptyChunk(Timestamp must not be negative: -2, Description must not be empty, Description must not be empty, Timestamp must not be negative: -5)) }
  • 56.
    Problems with Take2 ● It was a little difficult to implement collectEvents
  • 57.
    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)] }
  • 58.
    Solution: Traversable Typeclass ZIOPrelude has Traversable instances for Scala Standard Types: ● Option ● Either ● List ● Vector ● Map
  • 59.
    Solution: Traversable Typeclass And,of course, we can create instances of Traversable for our own collection types!
  • 60.
    Take 3 def main(args:Array[String]): Unit = { val rawEvents: List[(Int, String)] = List( (1000, "Event 1"), (2000, "Event 2"), (3000, "Event 3"), (4000, "Event 4"), (5000, "Event 5") ) val rawEvents2: List[(Int, String)] = List( (1000, "Event 1"), (-2, ""), (3000, ""), (4000, "Event 4"), (-5, "Event 5") ) val validatedEvents = Traversable[List].foreach(rawEvents) { case (timestamp, description) => Event.make(timestamp, description) } val validatedEvents2 = Traversable[List].foreach(rawEvents2) { case (timestamp, description) => Event.make(timestamp, description) } println(validatedEvents) // Success(List(Event(1000,Event 1), Event(2000,Event 2), Event(3000,Event 3), Event(4000,Event 4), Event(5000,Event 5))) println(validatedEvents2) // Failure(NonEmptyChunk(Timestamp must not be negative: -2, Description must not be empty, Description must not be empty, Timestamp must not be negative: -5)) }
  • 61.
    Example 2 Traversing abinary tree of Futures
  • 62.
    Solution import zio.prelude._ sealed traitBinaryTree[+A] { self => def map[B](f: A => B): BinaryTree[B] = self match { case BinaryTree.Leaf(a) => BinaryTree.Leaf(f(a)) case BinaryTree.Branch(left, right) => BinaryTree.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: Traversable[BinaryTree] = 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(_, _)) } } }
  • 63.
    Solution implicit val ec= ExecutionContext.fromExecutor(Executors.newFixedThreadPool(5)) def echo(message: String): Future[String] = Future { Thread.sleep(1000) s"Echo: $message" } def main(args: Array[String]): Unit = { 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")) ) ) */ }
  • 64.
    Traversable benefits ● Workson all Scala collection types ● Just by implementing foreach, we get a lot of additional methods for free!
  • 65.
  • 66.
    Example Write a programthat, given an user ID, looks for the user's name and age, and then: ● If user’s name is Jorge: The program should fail with an error message saying that you can't ask for Jorge's age. ● Otherwise: ○ If user's age is less than 50: The program should succeed with a log message including name and age ○ Otherwise: The program should succeed with no value
  • 67.
    Take 1: MonadTransformers val getUserName: Reader[Long, String] = Reader { id => if (1 <= id && id < 100) "Jorge" else if (100 <= id && id < 200) "Anne" else "Claire" } val getUserAge: Reader[Long, Int] = Reader { id => if (1 <= id && id < 100) 30 else if (100 <= id && id < 200) 50 else 20 } def log(msg: String): String = s"Log: $msg" val program: OptionT[EitherT[Reader[Long, *], String, *], String] = for { userName <- OptionT.liftF[EitherT[Reader[Long, *], String, *], String](EitherT.liftF[Reader[Long, *], String, String](getUserName)) userAge <- OptionT.liftF[EitherT[Reader[Long, *], String, *], Int](EitherT.liftF[Reader[Long, *], String, Int](getUserAge)) logMessage <- if (userName == "Jorge") OptionT.liftF[EitherT[Reader[Long, *], String, *], String]( EitherT.leftT[Reader[Long, *], String]("You are not allowed to know Jorge's age!") ) else if (userAge < 50) OptionT.some[EitherT[Reader[Long, *], String, *]](s"Name: $userName, Age: $userAge") else OptionT.none[EitherT[Reader[Long, *], String, *], String] log <- OptionT.liftF[EitherT[Reader[Long, *], String, *], String](EitherT.rightT[Reader[Long, *], String](log(logMessage))) } yield log def runProgram(id: Long): Either[String, Option[String]] = program.value.value.run(id)
  • 68.
    Take 1: MonadTransformers println(runProgram(10)) // Left(You are not allowed to know Jorge's age!) println(runProgram(150)) // Right(None) println(runProgram(250)) // Right(Some(Log: Name: Claire, Age: 20))
  • 69.
    Problems with Take1: Monad Transformers ● Complicated ● Types are not inferred (at all) ● Slow
  • 70.
  • 71.
    Solution: ZPure ZPure[S1, S2,R, E, A] is a purely functional description of a computation that requires an environment R and an initial state S1 and may either fail with an E or succeed with an updated state S2 and an A. Because of its polymorphism ZPure can be used to model a variety of effects including context, state, and failure.
  • 72.
    ZPure Mental Model R=> S1 => (S2, Either[E, A])
  • 73.
    ZPure type aliases typeEState[S, +E, +A] = ZPure[S, S, Any, E, A] type State[S, +A] = ZPure[S, S, Any, Nothing, A] type Reader[-R, +A] = ZPure[Unit, Unit, R, Nothing, A] type EReader[-R, +E, +A] = ZPure[Unit, Unit, R, E, A]
  • 74.
    Take 2: ZPure importzio.prelude._ val getUserName: Reader[Long, String] = Reader.fromFunction { id => if (1 <= id && id < 100) "Jorge" else if (100 <= id && id < 200) "Anne" else "Claire" } val getUserAge: Reader[Long, Int] = Reader.fromFunction { id => if (1 <= id && id < 100) 30 else if (100 <= id && id < 200) 50 else 20 } def log(msg: String): String = s"Log: $msg" val program: EReader[Long, String, Option[String]] = for { userName <- getUserName userAge <- getUserAge logMessage <- if (userName == "Jorge") ZPure.fail("You are not allowed to know Jorge's age!") else if (userAge < 50) ZPure.succeed[Unit, String](s"Name: $userName, Age: $userAge").asSome else ZPure.succeed[Unit, Option[String]](None) } yield logMessage.map(log) def runProgram(id: Long): Either[String, Option[String]] = program.provide(id).runEither(()).map(_._2)
  • 75.
    Take 2: ZPure println(runProgram(10))// Left(You are not allowed to know Jorge's age!) println(runProgram(150)) // Right(None) println(runProgram(250)) // Right(Some(Log: Name: Claire, Age: 20))
  • 76.
    ZPure benefits ● Simple ●Great type inference ● Fast!
  • 77.
    Special thanks ● ToScalac Functional World for hosting this presentation ● To John De Goes for guidance and technical checking
  • 78.
  • 79.
    Where to learnmore ● SF Scala: Reimagining Functional Type Classes, talk by John De Goes and Adam Fraser
  • 80.
  • 81.