Make it testable with ZIO
Environment
Wiem Zine Elabidine
Berlin Scala User Group
@WiemZin
@wi101
Make it testable with ZIO Environment
Introduction ZIO-Environment
2
Tagless Final Wrap Up
Statements
“imperative programming is a programming
paradigm that uses statements that change a
program's state”
3
def greeting(): Unit = {
println("Hello what is your name?")
val name = readLine()
println(s"Nice to meet you $name")
}
> Hello what's your name?
> |
Programming using Statements
44
> Hello what's your name?
> |
Test effectsControl effects
55
Test effectsControl effects
greeting() shouldBe {
["Good morning, what is your name?",
"nice to meet you user-test”]
}
Programming using Statements
Interface
6
trait Console {
def println(line: String): Unit
def readLine: String
}
Program
7
def greeting(console: Console): Unit = {
console.println("Good morning, "what is your name?")
val name = console.readLine()
console.println(s"Good to meet you, $name!")
}
Test Console Impl
8
var outputs: Vector[String] = Vector.empty
object TestConsole extends Console {
def println(line: String): Unit = outputs = outputs :+ line
def readLine: String = "test"
}
Prod Console Impl
9
object ProdConsole extends Console {
def println(line: String): Unit = scala.Console.println(line)
def readLine: String = scala.io.StdIn.readLine
}
Effects !
var outputs: Vector[String] = Vector.empty
// In PROD:
greeting(ProdConsole)
// In TEST:
greeting(TestConsole)
outputs shouldBe
Vector("Good morning, what is your name?",
"nice to meet you user-test”)
10
PROD
TEST
Using statements
1111
Test effectsControl effects
Functional Programming
12
f(a) g(b)
a b c
Functional Programming
13
● Declarative paradigm
● Pure Functions
● Immutable Data Structures
❏ handle events
❏ read from the DataBase
❏ persist information
❏ print out the result
❏ retry in event of errors
❏ send result to other
services
TO DO
Functional Programming
14
● Declarative paradigm
● Pure Functions
● Immutable Data Structures
- Total
- Deterministic
- Free of side effects
Functional Programming
15
● Declarative paradigm
● Pure Functions
● Immutable Data Structures
❏ handle events
❏ read from the DataBase
❏ persist information
❏ print out the result
❏ retry in event of errors
❏ send result to other
services
TO DO
Functional Programming
16
● Declarative paradigm
● Pure Functions
● Immutable Data Structures
Functional Programming
17
● Declarative paradigm
● Pure Functions
● Immutable Data Structures
class IO[A](val unsafeRun: () => A) { s =>
def map[B](f: A => B) =
ïŹ‚atMap(f.andThen(IO.effect(_)))
def ïŹ‚atMap[B](f: A => IO[B]): IO[B] =
IO.effect(f(s.unsafeRun()).unsafeRun())
}
object IO {
def effect[A](eff: => A) = new IO(() => eff)
}
18
Zero dependency Scala library for asynchronous and concurrent
programming based on pure functional programming.
● type-safe ● testable ● composable ● compatible to FP libraries
Functional effects
19
ZIO[R, E, A]
Environment Error Success
Functional effects
20
ZIO[R, Throwable, A]
TaskR[R, A]
0
Functional effects
21
ZIO[Any, Throwable, A]
Task[A]
0
Functional effects
22
ZIO[Any, E, A]
IO[E, A]
0
Functional effects
23
ZIO[Any, Nothing, A]
UIO[A]
0
Describe programs
24
val greeting: UIO[Unit] =
for {
_ <- UIO(println("Hello what is your name?"))
name <- Task(readLine())
_ <- UIO(println(s"Nice to meet you $name"))
} yield ()
Describe programs
25
val greeting: UIO[Unit] =
for {
_ <- UIO(println("Hello what is your name?"))
name <- Task(readLine())
_ <- UIO(println(s"Nice to meet you $name"))
} yield ()
Execute effects
26
val greeting: UIO[Unit] =
unsafeRun(
for {
_ <- UIO(println("Hello what is your name?"))
name <- Task(readLine())
_ <- UIO(println(s"Nice to meet you $name"))
} yield ())
> Hello what's your name?
> |
Using functional effects
2727
Test effectsControl effects
ZIO-Environment
28
ZIO[R, E, A]
Environment Error Success
Make it testable with ZIO Environment
Introduction
29
Tagless Final
ZIO-Environment
Wrap Up
Console interface
30
trait Console[F[_]] {
def println(line: String): F[Unit]
val readLine: F[String]
}
object Console {
def apply[F[_]](implicit F: Console[F]) = F
}
Polymorphic effect program
31
def greeting[F[_]: Monad](console: Console[F]): F[Unit] =
for {
_ <- console.println("Hello what is your name?")
name <- console.readLine
_ <- console.println(s"Nice to meet you $name")
} yield ()
Type class instances ~ Test
32
type Test[A] = State[Vector[String], A]
implicit val TestConsole = new Console[Test] {
def println(line: String): Test[Unit] =
State.modify(oldState => TestIO(oldState.outputs :+ line))
val readLine: Test[String] = State.get.map(_ => "test")
}
Type class instances ~ Test
33
type Test[A] = State[Vector[String], A]
implicit val TestConsole = new Console[Test] {
def println(line: String): Test[Unit] =
State.modify(oldState => TestIO(oldState.outputs :+ line))
val readLine: Test[String] = State.get.map(_ => "test")
}
Type class instances ~ Test
34
type Test[A] = State[Vector[String], A]
implicit val TestConsole = new Console[Test] {
def println(line: String): Test[Unit] =
State.modify(oldState => TestIO(oldState.outputs :+ line))
val readLine: Test[String] = State.get.map(_ => "test")
}
Applicative
Monad
Type class instances ~ Test
35
val (test, _) =
greeting(TestConsole).run(TestIO(Vector.empty))
→ sbt
> compile
...
Type class instances ~ Test
36
val (test, _) =
greeting(TestConsole).run(TestIO(Vector.empty))
value ïŹ‚atMap is not a member of type parameter F[Unit]
[error] Note: implicit value TestConsole is not applicable here because it
comes after the application point and it lacks an explicit result type
[error] _ <- console.println("Hello what is your name?")
value map is not a member of type parameter F[Unit]
[error] Note: implicit value TestConsole is not applicable here because it
comes after the application point and it lacks an explicit result type
[error] _ <- console.println(s"Nice to meet you $name")
Type class instances ~ Test
37
import scalaz.syntax.monad._
→ sbt
> compile


[info] Done compiling.
[success] Total time: 3 s, completed Jul 28, 2019 5:29:26 PM
Type class instances ~ Test
38
val (test, _) =
greeting(TestConsole).run(TestIO(Vector.empty))
greeting(TestConsole) shouldBe Vector(
"Good morning, what is your name?",
"nice to meet you test”)
Type class instances ~ Prod
39
val LiveConsole = new Console[Task] {
def println(line: String): UIO[Unit] = UIO(scala.Console.println(line))
val readLine: Task[String] = Task(scala.io.StdIn.readLine)
}
class ZIOMonad[R, E] extends Monad[ZIO[R, E, ?]] {
def point[A](a: => A): ZIO[R, E, A] = UIO(a)
def bind[A, B](fa: ZIO[R, E, A])(f: A => ZIO[R, E, B]): ZIO[R, E, B] = fa.ïŹ‚atMap(f)
}
val resultIO: Task[Unit] = greeting(LiveConsole)(new ZIOMonad[Any, Throwable])
Using many services
40
def program[F[_]: Monad : Console : ConïŹg : ...]
Using many services
41
def program[F[_]: Monad : Console : ConïŹg : ...]
[error] could not ïŹnd implicit value for evidence
parameter of type ConïŹg
Requirements
To use Tagless ïŹnal
1
Functional Effects
2
F[ _ ]
3
Type classes
42
Type class instancesIO[ E, ?]
Either[Error, ?]
Make it testable with ZIO Environment
Introduction
43
Tagless Final
ZIO-Environment
Wrap Up
ZIO-Environment
44
ZIO[R, E, A]
Environment Error Success
API
45
ZIO[R, E, A]
sealed trait ZIO[-R, +E, +A] {
def provide(environment: R): ZIO[Any, E, A]
}
object ZIO {
def accessM[R, E, A](f: R => IO[E, A]): ZIO[R, E, A]
def access[R, E, A](f: R => A): ZIO[R, E, A]
def environment[R]: ZIO[R, Nothing, R]
}
R => IO[E, A]
Console Service
46
trait Console {
val console: Console.Service[Any]
}
object Console {
trait Service[R] {
def println(line: String): ZIO[R, Nothing, Unit]
val readLine: TaskR[R, String]
}
}
Test Module
47
case class Test(ref: Ref[Vector[String]]) extends Console {
val console: Service[Any] = new Service[Any] {
def println(line: String): UIO[Unit] = ref.update(_ :+ line).unit
val readLine: UIO[String] = UIO("test")
}
}
Prod Module
48
trait Live extends Console {
val console: Service[Any] = new Service[Any] {
def println(line: String): UIO[Unit] = UIO(scala.Console.println(line))
val readLine: Task[String] = Task(scala.io.StdIn.readLine)
}
}
Console helper
49
package console extends Console.Service[Console] {
def println(line: String): ZIO[Console, Nothing, Unit] =
ZIO.accessM(_.console println line)
val readLine: TaskR[Console, String] = ZIO.accessM(_.console.readLine)
}
Program
50
val greeting: TaskR[Console, Unit] =
for {
_ <- console.println("Hello what is your name?")
name <- console.readLine
_ <- console.println(s"Nice to meet you $name")
} yield ()
Test vs Prod
for {
state <-
Ref.make(Vector.empty[String])
_ <- greeting.provide(Test(state))
result <- state.get
} yield result shouldBe Vector(
"Good morning, what is your name?",
"nice to meet you user-test”)
val program =
greeting.provide(Live)
51
Requirements
52
To use ZIO-Environment
Program with
Multi services
def program(id: UserId): TaskR[ConïŹg with Http with Database with Clock, Unit] =
for {
conf <- conïŹguration.loadConïŹg
user <-
ZIO.bracket(
runServer(conf),
_.close,
server =>
http.getUser(id)(server)
.retry(Schedule.doWhile(_.status == CommunicationError.status)))
_ <- db.create(user)
now <- clock.currentTime
_ <- console.putStrLn(s"$now User has been added successfully")
} yield ()
53
//TODO error
Program with
Multi services
def program(id: UserId): TaskR[ConïŹg with Http with Database with Clock, Unit] =
for {
conf <- conïŹguration.loadConïŹg
user <-
ZIO.bracket(
runServer(conf),
_.close,
server =>
http.getUser(id)(server)
.retry(Schedule.doWhile(_.status == CommunicationError.status)))
_ <- db.create(user)
now <- clock.currentTime
_ <- console.putStrLn(s"$now User has been added successfully")
} yield ()
54
type mismatch;
[error] found : scalaz.zio.Task[ConïŹg with Http with
Database with Clock, Unit]
[error] required: sscalaz.zio.Task[ConïŹg with Http with
Database with Clock with Console, Unit]
Program with
Multi services
def program(id: UserId): TaskR[ConïŹg with Http with Database with Clock with Console, Unit] =
for {
conf <- conïŹguration.loadConïŹg
user <-
ZIO.bracket(
runServer(conf),
_.close,
server =>
http.getUser(id)(server)
.retry(Schedule.doWhile(_.status == CommunicationError.status)))
_ <- db.create(user)
now <- clock.currentTime
_ <- console.putStrLn(s"$now User has been added successfully")
} yield ()
55
Test
program(UserId(1)).provide{
new ConïŹg.Test with
Http.Test with
Database.Test with
Console.Test with
Clock.Test
}
56
Prod
program(UserId(1)).provide{
new ConïŹg.Live with
Http.Live with
Database.Live with
Console.Live with
Clock.Live
}
57
Wrap Up
58
HTTP
Database
ConïŹg
Console
Clock
IO
Links
● Testing Incrementally with ZIO Environment: http://degoes.net/
● The death of ïŹnal tagless: https://skillsmatter.com/skillscasts/13247-scala-matters
● ZIO with http4s and doobie: https://medium.com/@wiemzin
59
ZIO
● ZIO: https://github.com/zio/zio
● Documentation: https://zio.dev/
● Gitter: https://gitter.im/ZIO/Core
60
THANK YOU!
Berlin Scala Meetup
@WiemZin
@wi101

Berlin meetup

  • 1.
    Make it testablewith ZIO Environment Wiem Zine Elabidine Berlin Scala User Group @WiemZin @wi101
  • 2.
    Make it testablewith ZIO Environment Introduction ZIO-Environment 2 Tagless Final Wrap Up
  • 3.
    Statements “imperative programming isa programming paradigm that uses statements that change a program's state” 3 def greeting(): Unit = { println("Hello what is your name?") val name = readLine() println(s"Nice to meet you $name") } > Hello what's your name? > |
  • 4.
    Programming using Statements 44 >Hello what's your name? > | Test effectsControl effects
  • 5.
    55 Test effectsControl effects greeting()shouldBe { ["Good morning, what is your name?", "nice to meet you user-test”] } Programming using Statements
  • 6.
    Interface 6 trait Console { defprintln(line: String): Unit def readLine: String }
  • 7.
    Program 7 def greeting(console: Console):Unit = { console.println("Good morning, "what is your name?") val name = console.readLine() console.println(s"Good to meet you, $name!") }
  • 8.
    Test Console Impl 8 varoutputs: Vector[String] = Vector.empty object TestConsole extends Console { def println(line: String): Unit = outputs = outputs :+ line def readLine: String = "test" }
  • 9.
    Prod Console Impl 9 objectProdConsole extends Console { def println(line: String): Unit = scala.Console.println(line) def readLine: String = scala.io.StdIn.readLine }
  • 10.
    Effects ! var outputs:Vector[String] = Vector.empty // In PROD: greeting(ProdConsole) // In TEST: greeting(TestConsole) outputs shouldBe Vector("Good morning, what is your name?", "nice to meet you user-test”) 10 PROD TEST
  • 11.
  • 12.
  • 13.
    Functional Programming 13 ● Declarativeparadigm ● Pure Functions ● Immutable Data Structures ❏ handle events ❏ read from the DataBase ❏ persist information ❏ print out the result ❏ retry in event of errors ❏ send result to other services TO DO
  • 14.
    Functional Programming 14 ● Declarativeparadigm ● Pure Functions ● Immutable Data Structures - Total - Deterministic - Free of side effects
  • 15.
    Functional Programming 15 ● Declarativeparadigm ● Pure Functions ● Immutable Data Structures ❏ handle events ❏ read from the DataBase ❏ persist information ❏ print out the result ❏ retry in event of errors ❏ send result to other services TO DO
  • 16.
    Functional Programming 16 ● Declarativeparadigm ● Pure Functions ● Immutable Data Structures
  • 17.
    Functional Programming 17 ● Declarativeparadigm ● Pure Functions ● Immutable Data Structures class IO[A](val unsafeRun: () => A) { s => def map[B](f: A => B) = ïŹ‚atMap(f.andThen(IO.effect(_))) def ïŹ‚atMap[B](f: A => IO[B]): IO[B] = IO.effect(f(s.unsafeRun()).unsafeRun()) } object IO { def effect[A](eff: => A) = new IO(() => eff) }
  • 18.
    18 Zero dependency Scalalibrary for asynchronous and concurrent programming based on pure functional programming. ● type-safe ● testable ● composable ● compatible to FP libraries
  • 19.
    Functional effects 19 ZIO[R, E,A] Environment Error Success
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
    Describe programs 24 val greeting:UIO[Unit] = for { _ <- UIO(println("Hello what is your name?")) name <- Task(readLine()) _ <- UIO(println(s"Nice to meet you $name")) } yield ()
  • 25.
    Describe programs 25 val greeting:UIO[Unit] = for { _ <- UIO(println("Hello what is your name?")) name <- Task(readLine()) _ <- UIO(println(s"Nice to meet you $name")) } yield ()
  • 26.
    Execute effects 26 val greeting:UIO[Unit] = unsafeRun( for { _ <- UIO(println("Hello what is your name?")) name <- Task(readLine()) _ <- UIO(println(s"Nice to meet you $name")) } yield ()) > Hello what's your name? > |
  • 27.
    Using functional effects 2727 TesteffectsControl effects
  • 28.
  • 29.
    Make it testablewith ZIO Environment Introduction 29 Tagless Final ZIO-Environment Wrap Up
  • 30.
    Console interface 30 trait Console[F[_]]{ def println(line: String): F[Unit] val readLine: F[String] } object Console { def apply[F[_]](implicit F: Console[F]) = F }
  • 31.
    Polymorphic effect program 31 defgreeting[F[_]: Monad](console: Console[F]): F[Unit] = for { _ <- console.println("Hello what is your name?") name <- console.readLine _ <- console.println(s"Nice to meet you $name") } yield ()
  • 32.
    Type class instances~ Test 32 type Test[A] = State[Vector[String], A] implicit val TestConsole = new Console[Test] { def println(line: String): Test[Unit] = State.modify(oldState => TestIO(oldState.outputs :+ line)) val readLine: Test[String] = State.get.map(_ => "test") }
  • 33.
    Type class instances~ Test 33 type Test[A] = State[Vector[String], A] implicit val TestConsole = new Console[Test] { def println(line: String): Test[Unit] = State.modify(oldState => TestIO(oldState.outputs :+ line)) val readLine: Test[String] = State.get.map(_ => "test") }
  • 34.
    Type class instances~ Test 34 type Test[A] = State[Vector[String], A] implicit val TestConsole = new Console[Test] { def println(line: String): Test[Unit] = State.modify(oldState => TestIO(oldState.outputs :+ line)) val readLine: Test[String] = State.get.map(_ => "test") } Applicative Monad
  • 35.
    Type class instances~ Test 35 val (test, _) = greeting(TestConsole).run(TestIO(Vector.empty)) → sbt > compile ...
  • 36.
    Type class instances~ Test 36 val (test, _) = greeting(TestConsole).run(TestIO(Vector.empty)) value ïŹ‚atMap is not a member of type parameter F[Unit] [error] Note: implicit value TestConsole is not applicable here because it comes after the application point and it lacks an explicit result type [error] _ <- console.println("Hello what is your name?") value map is not a member of type parameter F[Unit] [error] Note: implicit value TestConsole is not applicable here because it comes after the application point and it lacks an explicit result type [error] _ <- console.println(s"Nice to meet you $name")
  • 37.
    Type class instances~ Test 37 import scalaz.syntax.monad._ → sbt > compile 
 [info] Done compiling. [success] Total time: 3 s, completed Jul 28, 2019 5:29:26 PM
  • 38.
    Type class instances~ Test 38 val (test, _) = greeting(TestConsole).run(TestIO(Vector.empty)) greeting(TestConsole) shouldBe Vector( "Good morning, what is your name?", "nice to meet you test”)
  • 39.
    Type class instances~ Prod 39 val LiveConsole = new Console[Task] { def println(line: String): UIO[Unit] = UIO(scala.Console.println(line)) val readLine: Task[String] = Task(scala.io.StdIn.readLine) } class ZIOMonad[R, E] extends Monad[ZIO[R, E, ?]] { def point[A](a: => A): ZIO[R, E, A] = UIO(a) def bind[A, B](fa: ZIO[R, E, A])(f: A => ZIO[R, E, B]): ZIO[R, E, B] = fa.ïŹ‚atMap(f) } val resultIO: Task[Unit] = greeting(LiveConsole)(new ZIOMonad[Any, Throwable])
  • 40.
    Using many services 40 defprogram[F[_]: Monad : Console : ConïŹg : ...]
  • 41.
    Using many services 41 defprogram[F[_]: Monad : Console : ConïŹg : ...] [error] could not ïŹnd implicit value for evidence parameter of type ConïŹg
  • 42.
    Requirements To use TaglessïŹnal 1 Functional Effects 2 F[ _ ] 3 Type classes 42 Type class instancesIO[ E, ?] Either[Error, ?]
  • 43.
    Make it testablewith ZIO Environment Introduction 43 Tagless Final ZIO-Environment Wrap Up
  • 44.
  • 45.
    API 45 ZIO[R, E, A] sealedtrait ZIO[-R, +E, +A] { def provide(environment: R): ZIO[Any, E, A] } object ZIO { def accessM[R, E, A](f: R => IO[E, A]): ZIO[R, E, A] def access[R, E, A](f: R => A): ZIO[R, E, A] def environment[R]: ZIO[R, Nothing, R] } R => IO[E, A]
  • 46.
    Console Service 46 trait Console{ val console: Console.Service[Any] } object Console { trait Service[R] { def println(line: String): ZIO[R, Nothing, Unit] val readLine: TaskR[R, String] } }
  • 47.
    Test Module 47 case classTest(ref: Ref[Vector[String]]) extends Console { val console: Service[Any] = new Service[Any] { def println(line: String): UIO[Unit] = ref.update(_ :+ line).unit val readLine: UIO[String] = UIO("test") } }
  • 48.
    Prod Module 48 trait Liveextends Console { val console: Service[Any] = new Service[Any] { def println(line: String): UIO[Unit] = UIO(scala.Console.println(line)) val readLine: Task[String] = Task(scala.io.StdIn.readLine) } }
  • 49.
    Console helper 49 package consoleextends Console.Service[Console] { def println(line: String): ZIO[Console, Nothing, Unit] = ZIO.accessM(_.console println line) val readLine: TaskR[Console, String] = ZIO.accessM(_.console.readLine) }
  • 50.
    Program 50 val greeting: TaskR[Console,Unit] = for { _ <- console.println("Hello what is your name?") name <- console.readLine _ <- console.println(s"Nice to meet you $name") } yield ()
  • 51.
    Test vs Prod for{ state <- Ref.make(Vector.empty[String]) _ <- greeting.provide(Test(state)) result <- state.get } yield result shouldBe Vector( "Good morning, what is your name?", "nice to meet you user-test”) val program = greeting.provide(Live) 51
  • 52.
  • 53.
    Program with Multi services defprogram(id: UserId): TaskR[ConïŹg with Http with Database with Clock, Unit] = for { conf <- conïŹguration.loadConïŹg user <- ZIO.bracket( runServer(conf), _.close, server => http.getUser(id)(server) .retry(Schedule.doWhile(_.status == CommunicationError.status))) _ <- db.create(user) now <- clock.currentTime _ <- console.putStrLn(s"$now User has been added successfully") } yield () 53
  • 54.
    //TODO error Program with Multiservices def program(id: UserId): TaskR[ConïŹg with Http with Database with Clock, Unit] = for { conf <- conïŹguration.loadConïŹg user <- ZIO.bracket( runServer(conf), _.close, server => http.getUser(id)(server) .retry(Schedule.doWhile(_.status == CommunicationError.status))) _ <- db.create(user) now <- clock.currentTime _ <- console.putStrLn(s"$now User has been added successfully") } yield () 54 type mismatch; [error] found : scalaz.zio.Task[ConïŹg with Http with Database with Clock, Unit] [error] required: sscalaz.zio.Task[ConïŹg with Http with Database with Clock with Console, Unit]
  • 55.
    Program with Multi services defprogram(id: UserId): TaskR[ConïŹg with Http with Database with Clock with Console, Unit] = for { conf <- conïŹguration.loadConïŹg user <- ZIO.bracket( runServer(conf), _.close, server => http.getUser(id)(server) .retry(Schedule.doWhile(_.status == CommunicationError.status))) _ <- db.create(user) now <- clock.currentTime _ <- console.putStrLn(s"$now User has been added successfully") } yield () 55
  • 56.
    Test program(UserId(1)).provide{ new ConïŹg.Test with Http.Testwith Database.Test with Console.Test with Clock.Test } 56
  • 57.
    Prod program(UserId(1)).provide{ new ConïŹg.Live with Http.Livewith Database.Live with Console.Live with Clock.Live } 57
  • 58.
  • 59.
    Links ● Testing Incrementallywith ZIO Environment: http://degoes.net/ ● The death of ïŹnal tagless: https://skillsmatter.com/skillscasts/13247-scala-matters ● ZIO with http4s and doobie: https://medium.com/@wiemzin 59
  • 60.
    ZIO ● ZIO: https://github.com/zio/zio ●Documentation: https://zio.dev/ ● Gitter: https://gitter.im/ZIO/Core 60
  • 61.
    THANK YOU! Berlin ScalaMeetup @WiemZin @wi101