Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Implementing the IO Monad in Scala

498 views

Published on

With my simple implementation I wanted to demonstrate the basic ideas of th IO Monad.

My impl of the IO Monad is just a feasibility study, not production code!

When coding my impl of IO I was very much inspired by cats.effect.IO and monix.eval.Task which I studied at that time. Both are implementions of the IO Monad.

The API of my IO is very similar to the basics of Monix Task. This IO implementation also helped me to understand the IO Monad (of cats-effect) and Monix Task.

Interop with Future is also supported. You can convert IO to a Future. Vice versa you can convert a Future to an IO.

The development of my impl can be followed step by step in the code files in package iomonad.

Published in: Software
  • DOWNLOAD THIS BOOKS INTO AVAILABLE FORMAT (2019 Update) ......................................................................................................................... ......................................................................................................................... Download Full PDF EBOOK here { https://soo.gd/irt2 } ......................................................................................................................... Download Full EPUB Ebook here { https://soo.gd/irt2 } ......................................................................................................................... Download Full doc Ebook here { https://soo.gd/irt2 } ......................................................................................................................... Download PDF EBOOK here { https://soo.gd/irt2 } ......................................................................................................................... Download EPUB Ebook here { https://soo.gd/irt2 } ......................................................................................................................... Download doc Ebook here { https://soo.gd/irt2 } ......................................................................................................................... ......................................................................................................................... ................................................................................................................................... eBook is an electronic version of a traditional print book THIS can be read by using a personal computer or by using an eBook reader. (An eBook reader can be a software application for use on a computer such as Microsoft's free Reader application, or a book-sized computer THIS is used solely as a reading device such as Nuvomedia's Rocket eBook.) Users can purchase an eBook on diskette or CD, but the most popular method of getting an eBook is to purchase a downloadable file of the eBook (or other reading material) from a Web site (such as Barnes and Noble) to be read from the user's computer or reading device. Generally, an eBook can be downloaded in five minutes or less ......................................................................................................................... .............. Browse by Genre Available eBooks .............................................................................................................................. Art, Biography, Business, Chick Lit, Children's, Christian, Classics, Comics, Contemporary, Cookbooks, Manga, Memoir, Music, Mystery, Non Fiction, Paranormal, Philosophy, Poetry, Psychology, Religion, Romance, Science, Science Fiction, Self Help, Suspense, Spirituality, Sports, Thriller, Travel, Young Adult, Crime, Ebooks, Fantasy, Fiction, Graphic Novels, Historical Fiction, History, Horror, Humor And Comedy, ......................................................................................................................... ......................................................................................................................... .....BEST SELLER FOR EBOOK RECOMMEND............................................................. ......................................................................................................................... Blowout: Corrupted Democracy, Rogue State Russia, and the Richest, Most Destructive Industry on Earth,-- The Ride of a Lifetime: Lessons Learned from 15 Years as CEO of the Walt Disney Company,-- Call Sign Chaos: Learning to Lead,-- StrengthsFinder 2.0,-- Stillness Is the Key,-- She Said: Breaking the Sexual Harassment Story THIS Helped Ignite a Movement,-- Atomic Habits: An Easy & Proven Way to Build Good Habits & Break Bad Ones,-- Everything Is Figureoutable,-- What It Takes: Lessons in the Pursuit of Excellence,-- Rich Dad Poor Dad: What the Rich Teach Their Kids About Money THIS the Poor and Middle Class Do Not!,-- The Total Money Makeover: Classic Edition: A Proven Plan for Financial Fitness,-- Shut Up and Listen!: Hard Business Truths THIS Will Help You Succeed, ......................................................................................................................... .........................................................................................................................
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • GIVE HER A BIGGER PACKAGE THIS VALENTINE'S DAY ●●● https://bit.ly/30G1ZO1
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • Did you know that once you lose your Ex, there is still a good chance you can get them back? Learn how ●●● http://goo.gl/nkXEkK
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

Implementing the IO Monad in Scala

  1. 1. Implementing the IO Monad © 2019 Hermann Hueck https://github.com/hermannhueck/implementing-io-monad 1 / 87
  2. 2. Abstract With my simple implementation I wanted to demonstrate the basic ideas of th IO Monad. My impl of the IO Monad is just a feasibility study, not production code! When coding my impl of IO I was very much inspired by cats.effect.IO and monix.eval.Task which I studied at that time. Both are implementions of the IO Monad. The API of my IO is very similar to the basics of Monix Task. This IO implementation also helped me to understand the IO Monad (of cats-effect) and Monix Task. Interop with Future is also supported. You can convert IO to a Future. Vice versa you can convert a Future to an IO. The development of my impl can be followed step by step in the code files in package iomonad. 2 / 87
  3. 3. Agenda 1. Referential Transparency 2. Is Future referentially transparent? 3. The IO Monad 4. Resources 3 / 87
  4. 4. 1. Referential Transparency 4 / 87
  5. 5. Referential Transparency An expression is called referentially transparent if it can be replaced with its corresponding value without changing the program's behavior. This requires that the expression is purepure, that is to say the expression value must be the same for the same inputs and its evaluation must have no side effectsno side effects. https://en.wikipedia.org/wiki/Referential_transparency 5 / 87
  6. 6. Referential Transparency Benefits (Equational) Reasoning about code Refactoring is easier Testing is easier Separate pure code from impure code Potential compiler optimizations (more in Haskell than in Scala) (e.g. memoization, parallelisation, compute expressions at compile time) "What Referential Transparency can do for you" Talk by Luka Jacobowitz at ScalaIO 2017 https://www.youtube.com/watch?v=X-cEGEJMx_4 6 / 87
  7. 7. func is not referentially transparent! def func(ioa1: Unit, ioa2: Unit): Unit = { ioa1 ioa2 } func(println("hi"), println("hi")) // prints "hi" twice //=> hi //=> hi println("-----") val x: Unit = println("hi") func(x, x) // prints "hi" once //=> hi 7 / 87
  8. 8. func is referentially transparent! def putStrLn(line: String): IO[Unit] = IO.eval { println(line) } def func(ioa1: IO[Unit], ioa2: IO[Unit]): IO[Unit] = for { _ <- ioa1 _ <- ioa2 } yield () func(putStrLn("hi"), putStrLn("hi")).run() // prints "hi" twice //=> hi //=> hi println("-----") val x: IO[Unit] = putStrLn("hi") func(x, x).run() // prints "hi" twice //=> hi //=> hi 8 / 87
  9. 9. 2. Is Future referentially transparent? 9 / 87
  10. 10. Is Future referentially transparent? val future1: Future[(Int, Int)] = { val atomicInt = new AtomicInteger(0) val future: Future[Int] = Future { atomicInt.incrementAndGet } for { x <- future y <- future } yield (x, y) } future1 onComplete println // Success((1,1)) // same as future1, but inlined val future2: Future[(Int, Int)] = { val atomicInt = new AtomicInteger(0) for { x <- Future { atomicInt.incrementAndGet } y <- Future { atomicInt.incrementAndGet } } yield (x, y) } future2 onComplete println // Success((1,2)) <-- not the same result 10 / 87
  11. 11. Is Future referentially transparent? val future1: Future[(Int, Int)] = { val atomicInt = new AtomicInteger(0) val future: Future[Int] = Future { atomicInt.incrementAndGet } for { x <- future y <- future } yield (x, y) } future1 onComplete println // Success((1,1)) // same as future1, but inlined val future2: Future[(Int, Int)] = { val atomicInt = new AtomicInteger(0) for { x <- Future { atomicInt.incrementAndGet } y <- Future { atomicInt.incrementAndGet } } yield (x, y) } future2 onComplete println // Success((1,2)) <-- not the same result No! 11 / 87
  12. 12. Is Monix Task referentially transparent? val task1: Task[(Int, Int)] = { val atomicInt = new AtomicInteger(0) val task: Task[Int] = Task { atomicInt.incrementAndGet } for { x <- task y <- task } yield (x, y) } task1 runAsync println // Success((1,2)) // same as task1, but inlined val task2: Task[(Int, Int)] = { val atomicInt = new AtomicInteger(0) for { x <- Task { atomicInt.incrementAndGet } y <- Task { atomicInt.incrementAndGet } } yield (x, y) } task2 runAsync println // Success((1,2)) <-- same result 12 / 87
  13. 13. Is Monix Task referentially transparent? val task1: Task[(Int, Int)] = { val atomicInt = new AtomicInteger(0) val task: Task[Int] = Task { atomicInt.incrementAndGet } for { x <- task y <- task } yield (x, y) } task1 runAsync println // Success((1,2)) // same as task1, but inlined val task2: Task[(Int, Int)] = { val atomicInt = new AtomicInteger(0) for { x <- Task { atomicInt.incrementAndGet } y <- Task { atomicInt.incrementAndGet } } yield (x, y) } task2 runAsync println // Success((1,2)) <-- same result Yes! 13 / 87
  14. 14. Is my IO Monad referentially transparent? val io1: IO[(Int, Int)] = { val atomicInt = new AtomicInteger(0) val io: IO[Int] = IO { atomicInt.incrementAndGet } for { x <- io y <- io } yield (x, y) } io1.runToFuture onComplete println // Success((1,2)) // same as io1, but inlined val io2: IO[(Int, Int)] = { val atomicInt = new AtomicInteger(0) for { x <- IO { atomicInt.incrementAndGet } y <- IO { atomicInt.incrementAndGet } } yield (x, y) } io2.runToFuture onComplete println // Success((1,2)) <-- same result 14 / 87
  15. 15. Is my IO Monad referentially transparent? val io1: IO[(Int, Int)] = { val atomicInt = new AtomicInteger(0) val io: IO[Int] = IO { atomicInt.incrementAndGet } for { x <- io y <- io } yield (x, y) } io1.runToFuture onComplete println // Success((1,2)) // same as io1, but inlined val io2: IO[(Int, Int)] = { val atomicInt = new AtomicInteger(0) for { x <- IO { atomicInt.incrementAndGet } y <- IO { atomicInt.incrementAndGet } } yield (x, y) } io2.runToFuture onComplete println // Success((1,2)) <-- same result Yes! 15 / 87
  16. 16. 3. The IO Monad 16 / 87
  17. 17. 1. Impure IO Program with side e!ects // impure program def program(): Unit = { print("Welcome to Scala! What's your name? ") val name = scala.io.StdIn.readLine println(s"Well hello, $name!") } 17 / 87
  18. 18. 1. Impure IO Program with side e!ects // impure program def program(): Unit = { print("Welcome to Scala! What's your name? ") val name = scala.io.StdIn.readLine println(s"Well hello, $name!") } program() 18 / 87
  19. 19. 1. Impure IO Program with side e!ects // impure program def program(): Unit = { print("Welcome to Scala! What's your name? ") val name = scala.io.StdIn.readLine println(s"Well hello, $name!") } program() Whenever a method or a function returns Unit it is impureimpure (or it is a noop). It's intension is to produce a side effect. A purepure function always returns a value of some type (and doesn't produce a side effect inside). 19 / 87
  20. 20. 2. Pure IO Program without side e!ects // pure program val program: () => Unit = // () => Unit is syntactic sugar for: Function0[Unit] () => { print("Welcome to Scala! What's your name? ") val name = scala.io.StdIn.readLine println(s"Well hello, $name!") } 20 / 87
  21. 21. 2. Pure IO Program without side e!ects // pure program val program: () => Unit = // () => Unit is syntactic sugar for: Function0[Unit] () => { print("Welcome to Scala! What's your name? ") val name = scala.io.StdIn.readLine println(s"Well hello, $name!") } program() // producing the side effects "at the end of the world" 21 / 87
  22. 22. 2. Pure IO Program without side e!ects // pure program val program: () => Unit = // () => Unit is syntactic sugar for: Function0[Unit] () => { print("Welcome to Scala! What's your name? ") val name = scala.io.StdIn.readLine println(s"Well hello, $name!") } program() // producing the side effects "at the end of the world" Make the program a function returning Unit: Function0[Unit] Free of side effects in it's definition Produces side effects only when run (at the end of the world) program can be a val (if no parameters are passed to it) 22 / 87
  23. 23. 3. Wrap Function0[A] in a case class case class IO[A](run: () => A) 23 / 87
  24. 24. 3. Wrap Function0[A] in a case class case class IO[A](run: () => A) // pure program val program: IO[Unit] = IO { () => { print("Welcome to Scala! What's your name? ") val name = scala.io.StdIn.readLine println(s"Well hello, $name!") } } 24 / 87
  25. 25. 3. Wrap Function0[A] in a case class case class IO[A](run: () => A) // pure program val program: IO[Unit] = IO { () => { print("Welcome to Scala! What's your name? ") val name = scala.io.StdIn.readLine println(s"Well hello, $name!") } } program.run() // producing the side effects "at the end of the world" 25 / 87
  26. 26. 3. Wrap Function0[A] in a case class case class IO[A](run: () => A) // pure program val program: IO[Unit] = IO { () => { print("Welcome to Scala! What's your name? ") val name = scala.io.StdIn.readLine println(s"Well hello, $name!") } } program.run() // producing the side effects "at the end of the world" IO[A] wraps a Function0[A] in a case class. This is useful to implement further extensions on that case class. 26 / 87
  27. 27. 4. IO#map and IO#flatMap case class IO[A](run: () => A) { def flatMap[B](f: A => IO[B]): IO[B] = IO { () => f(run()).run() } def map[B](f: A => B): IO[B] = IO { () => f(run()) } def flatten[B](implicit ev: A <:< IO[B]): IO[B] = flatMap(a => a) } 27 / 87
  28. 28. 4. IO#map and IO#flatMap case class IO[A](run: () => A) { def flatMap[B](f: A => IO[B]): IO[B] = IO { () => f(run()).run() } def map[B](f: A => B): IO[B] = IO { () => f(run()) } def flatten[B](implicit ev: A <:< IO[B]): IO[B] = flatMap(a => a) } val program: IO[Unit] = for { _ <- IO { () => print(s"Welcome to Scala! What's your name? ") } name <- IO { () => scala.io.StdIn.readLine } _ <- IO { () => println(s"Well hello, $name!") } } yield () 28 / 87
  29. 29. 4. IO#map and IO#flatMap case class IO[A](run: () => A) { def flatMap[B](f: A => IO[B]): IO[B] = IO { () => f(run()).run() } def map[B](f: A => B): IO[B] = IO { () => f(run()) } def flatten[B](implicit ev: A <:< IO[B]): IO[B] = flatMap(a => a) } val program: IO[Unit] = for { _ <- IO { () => print(s"Welcome to Scala! What's your name? ") } name <- IO { () => scala.io.StdIn.readLine } _ <- IO { () => println(s"Well hello, $name!") } } yield () program.run() // producing the side effects "at the end of the world" 29 / 87
  30. 30. 4. IO#map and IO#flatMap case class IO[A](run: () => A) { def flatMap[B](f: A => IO[B]): IO[B] = IO { () => f(run()).run() } def map[B](f: A => B): IO[B] = IO { () => f(run()) } def flatten[B](implicit ev: A <:< IO[B]): IO[B] = flatMap(a => a) } val program: IO[Unit] = for { _ <- IO { () => print(s"Welcome to Scala! What's your name? ") } name <- IO { () => scala.io.StdIn.readLine } _ <- IO { () => println(s"Well hello, $name!") } } yield () program.run() // producing the side effects "at the end of the world" With map and flatMap IO[A] is monadic (but it is not yet a Monad). IO is ready for for-comprehensions. This allows the composition of programs from smaller components. 30 / 87
  31. 31. 5. Companion object IO with pure and eval case class IO[A](run: () => A) { def flatMap[B](f: A => IO[B]): IO[B] = IO { () => f(run()).run() } def map[B](f: A => B): IO[B] = flatMap(a => pure(f(a))) def flatten[B](implicit ev: A <:< IO[B]): IO[B] = flatMap(a => a) } object IO { def pure[A](value: A): IO[A] = IO { () => value } // eager def eval[A](thunk: => A): IO[A] = IO { () => thunk } // lazy } 31 / 87
  32. 32. 5. Companion object IO with pure and eval case class IO[A](run: () => A) { def flatMap[B](f: A => IO[B]): IO[B] = IO { () => f(run()).run() } def map[B](f: A => B): IO[B] = flatMap(a => pure(f(a))) def flatten[B](implicit ev: A <:< IO[B]): IO[B] = flatMap(a => a) } object IO { def pure[A](value: A): IO[A] = IO { () => value } // eager def eval[A](thunk: => A): IO[A] = IO { () => thunk } // lazy } val program: IO[Unit] = for { welcome <- IO.pure("Welcome to Scala!") _ <- IO.eval { print(s"$welcome What's your name? ") } name <- IO.eval { scala.io.StdIn.readLine } // simpler with IO.eval _ <- IO.eval { println(s"Well hello, $name!") } } yield () 32 / 87
  33. 33. 5. Companion object IO with pure and eval case class IO[A](run: () => A) { def flatMap[B](f: A => IO[B]): IO[B] = IO { () => f(run()).run() } def map[B](f: A => B): IO[B] = flatMap(a => pure(f(a))) def flatten[B](implicit ev: A <:< IO[B]): IO[B] = flatMap(a => a) } object IO { def pure[A](value: A): IO[A] = IO { () => value } // eager def eval[A](thunk: => A): IO[A] = IO { () => thunk } // lazy } val program: IO[Unit] = for { welcome <- IO.pure("Welcome to Scala!") _ <- IO.eval { print(s"$welcome What's your name? ") } name <- IO.eval { scala.io.StdIn.readLine } // simpler with IO.eval _ <- IO.eval { println(s"Well hello, $name!") } } yield () program.run() // producing the side effects "at the end of the world" 33 / 87
  34. 34. 5. Companion object IO with pure and eval case class IO[A](run: () => A) { def flatMap[B](f: A => IO[B]): IO[B] = IO { () => f(run()).run() } def map[B](f: A => B): IO[B] = flatMap(a => pure(f(a))) def flatten[B](implicit ev: A <:< IO[B]): IO[B] = flatMap(a => a) } object IO { def pure[A](value: A): IO[A] = IO { () => value } // eager def eval[A](thunk: => A): IO[A] = IO { () => thunk } // lazy } val program: IO[Unit] = for { welcome <- IO.pure("Welcome to Scala!") _ <- IO.eval { print(s"$welcome What's your name? ") } name <- IO.eval { scala.io.StdIn.readLine } // simpler with IO.eval _ <- IO.eval { println(s"Well hello, $name!") } } yield () program.run() // producing the side effects "at the end of the world" IO.pure is eager and accepts a pure value. IO.eval is lazy and accepts a computation. map can be written in terms of flatMap and pure. 34 / 87
  35. 35. 6. ADT IO sealed trait IO[+A] { def run(): A def flatMap[B](f: A => IO[B]): IO[B] = IO { f(run()).run() } def map[B](f: A => B): IO[B] = flatMap(a => pure(f(a))) } object IO { // ADT sub types private case class Pure[A](thunk: () => A) extends IO[A] { override def run(): A = thunk() } private case class Eval[A](thunk: () => A) extends IO[A] { override def run(): A = thunk() } def pure[A](a: A): IO[A] = Pure { () => a } def now[A](a: A): IO[A] = pure(a) def eval[A](a: => A): IO[A] = Eval { () => a } def delay[A](a: => A): IO[A] = eval(a) def apply[A](a: => A): IO[A] = eval(a) } 35 / 87
  36. 36. 6. ADT IO sealed trait IO[+A] { def run(): A def flatMap[B](f: A => IO[B]): IO[B] = IO { f(run()).run() } def map[B](f: A => B): IO[B] = flatMap(a => pure(f(a))) } object IO { // ADT sub types private case class Pure[A](thunk: () => A) extends IO[A] { override def run(): A = thunk() } private case class Eval[A](thunk: () => A) extends IO[A] { override def run(): A = thunk() } def pure[A](a: A): IO[A] = Pure { () => a } def now[A](a: A): IO[A] = pure(a) def eval[A](a: => A): IO[A] = Eval { () => a } def delay[A](a: => A): IO[A] = eval(a) def apply[A](a: => A): IO[A] = eval(a) } IO.apply is an alias for IO.eval. (Simplifies the creation of IO instances.) The app works as before. 36 / 87
  37. 37. 7. ADT sub type FlatMap sealed trait IO[+A] { def flatMap[B](f: A => IO[B]): IO[B] = FlatMap(this, f) def map[B](f: A => B): IO[B] = flatMap(a => pure(f(a))) } object IO { // ADT sub types private case class FlatMap[A, B](src: IO[A], f: A => IO[B]) extends IO[B] { override def run(): B = f(src.run()).run() } } 37 / 87
  38. 38. 7. ADT sub type FlatMap sealed trait IO[+A] { def flatMap[B](f: A => IO[B]): IO[B] = FlatMap(this, f) def map[B](f: A => B): IO[B] = flatMap(a => pure(f(a))) } object IO { // ADT sub types private case class FlatMap[A, B](src: IO[A], f: A => IO[B]) extends IO[B] { override def run(): B = f(src.run()).run() } } We gained stack-safety by trampolining. (FlatMap is a heap object.) The app works as before. 38 / 87
  39. 39. IO Monad - Basic Setup A Function0 wrapped in a case class Impl of map and flatMap (and flatten) companion object with pure and other smart constructors case class converted to ADT 39 / 87
  40. 40. IO Monad - Basic Setup A Function0 wrapped in a case class Impl of map and flatMap (and flatten) companion object with pure and other smart constructors case class converted to ADT What is to come? Sync and async run methods More smart constructors Monad instance for IO Create IO from Try, Either, Future MonadError instance for IO (IO.raiseError, IO#handleErrorWith) Bracket instance for IO (IO#bracket) Sync instance for IO (IO.delay) 40 / 87
  41. 41. 8. Sync run* methods case class IO[A](run: () => A) { def runToTry: Try[A] = Try { run() } def runToEither: Either[Throwable, A] = runToTry.toEither } 41 / 87
  42. 42. 8. Sync run* methods case class IO[A](run: () => A) { def runToTry: Try[A] = Try { run() } def runToEither: Either[Throwable, A] = runToTry.toEither } // running the program synchronously ... val value: Unit = program.run() // () val tryy: Try[Unit] = program.runToTry // Success(()) val either: Either[Throwable, Unit] = program.runToEither // Right(()) 42 / 87
  43. 43. 8. Sync run* methods case class IO[A](run: () => A) { def runToTry: Try[A] = Try { run() } def runToEither: Either[Throwable, A] = runToTry.toEither } // running the program synchronously ... val value: Unit = program.run() // () val tryy: Try[Unit] = program.runToTry // Success(()) val either: Either[Throwable, Unit] = program.runToEither // Right(()) run may throw an exception. runToTry and runToEither avoid that. 43 / 87
  44. 44. 9. Other example: Authenticate Maggie def authenticate(username: String, password: String): IO[Boolean] = ??? val checkMaggie: IO[Boolean] = authenticate("maggie", "maggie-pw") // running checkMaggie synchronously ... val value: Boolean = checkMaggie.run() // true val tryy: Try[Boolean] = checkMaggie.runToTry // Success(true) val either: Either[Throwable, Boolean] = checkMaggie.runToEither // Right(true) 44 / 87
  45. 45. 9. Other example: Authenticate Maggie def authenticate(username: String, password: String): IO[Boolean] = ??? val checkMaggie: IO[Boolean] = authenticate("maggie", "maggie-pw") // running checkMaggie synchronously ... val value: Boolean = checkMaggie.run() // true val tryy: Try[Boolean] = checkMaggie.runToTry // Success(true) val either: Either[Throwable, Boolean] = checkMaggie.runToEither // Right(true) The previous example was interactive ... not well suited for async IO. 45 / 87
  46. 46. 10. Async run* methods case class IO[A](run: () => A) { def runToFuture(implicit ec: ExecutionContext): Future[A] = Future { run() } def runOnComplete(callback: Try[A] => Unit) (implicit ec: ExecutionContext): Unit = runToFuture onComplete callback def runAsync(callback: Either[Throwable, A] => Unit) (implicit ec: ExecutionContext): Unit = runOnComplete(tryy => callback(tryy.toEither)) } 46 / 87
  47. 47. 10. Async run* methods case class IO[A](run: () => A) { def runToFuture(implicit ec: ExecutionContext): Future[A] = Future { run() } def runOnComplete(callback: Try[A] => Unit) (implicit ec: ExecutionContext): Unit = runToFuture onComplete callback def runAsync(callback: Either[Throwable, A] => Unit) (implicit ec: ExecutionContext): Unit = runOnComplete(tryy => callback(tryy.toEither)) } All async run* methods take an implicit EC. Unlike Future the EC is not needed to create an IO, only to run it. 47 / 87
  48. 48. Using the async run* methods def authenticate(username: String, password: String): IO[Boolean] = ??? val checkMaggie: IO[Boolean] = authenticate("maggie", "maggie-pw") 48 / 87
  49. 49. Using the async run* methods def authenticate(username: String, password: String): IO[Boolean] = ??? val checkMaggie: IO[Boolean] = authenticate("maggie", "maggie-pw") // running 'checkMaggie' asynchronously ... implicit val ec: ExecutionContext = ExecutionContext.global val future: Future[Boolean] = checkMaggie.runToFuture future onComplete tryCallback //=> true checkMaggie runOnComplete tryCallback //=> true checkMaggie runAsync eitherCallback //=> true 49 / 87
  50. 50. Using the async run* methods def authenticate(username: String, password: String): IO[Boolean] = ??? val checkMaggie: IO[Boolean] = authenticate("maggie", "maggie-pw") // running 'checkMaggie' asynchronously ... implicit val ec: ExecutionContext = ExecutionContext.global val future: Future[Boolean] = checkMaggie.runToFuture future onComplete tryCallback //=> true checkMaggie runOnComplete tryCallback //=> true checkMaggie runAsync eitherCallback //=> true def tryCallback[A]: Try[A] => Unit = tryy => println(tryy.fold(ex => ex.toString, value => value.toString)) def eitherCallback[A]: Either[Throwable, A] => Unit = either => println(either.fold(ex => ex.toString, value => value.toString)) 50 / 87
  51. 51. 11. Async method foreach case class IO[A](run: () => A) { def foreach(f: A => Unit)(implicit ec: ExecutionContext): Unit = runAsync { case Left(ex) => ec.reportFailure(ex) case Right(value) => f(value) } } 51 / 87
  52. 52. 11. Async method foreach case class IO[A](run: () => A) { def foreach(f: A => Unit)(implicit ec: ExecutionContext): Unit = runAsync { case Left(ex) => ec.reportFailure(ex) case Right(value) => f(value) } } authenticate("maggie", "maggie-pw") foreach println //=> true authenticate("maggieXXX", "maggie-pw") foreach println //=> false authenticate("maggie", "maggie-pwXXX") foreach println //=> false 52 / 87
  53. 53. 11. Async method foreach case class IO[A](run: () => A) { def foreach(f: A => Unit)(implicit ec: ExecutionContext): Unit = runAsync { case Left(ex) => ec.reportFailure(ex) case Right(value) => f(value) } } authenticate("maggie", "maggie-pw") foreach println //=> true authenticate("maggieXXX", "maggie-pw") foreach println //=> false authenticate("maggie", "maggie-pwXXX") foreach println //=> false foreach runs asynchronously. It takes a callback for the successful result value. foreach swallows exceptions. Prefer runAsync! 53 / 87
  54. 54. 12. IO.raiseError object IO { // ADT sub types private case class Error[A](exception: Throwable) extends IO[A] { override def run(): A = throw exception } def raiseError[A](exception: Exception): IO[A] = Error[A](exception) } 54 / 87
  55. 55. 12. IO.raiseError object IO { // ADT sub types private case class Error[A](exception: Throwable) extends IO[A] { override def run(): A = throw exception } def raiseError[A](exception: Exception): IO[A] = Error[A](exception) } ADT sub type Error wraps a Throwable. 55 / 87
  56. 56. 12. IO.raiseError object IO { // ADT sub types private case class Error[A](exception: Throwable) extends IO[A] { override def run(): A = throw exception } def raiseError[A](exception: Exception): IO[A] = Error[A](exception) } ADT sub type Error wraps a Throwable. val ioError: IO[Int] = IO.raiseError[Int]( new IllegalStateException("illegal state")) println(ioError.runToEither) //=> Left(java.lang.IllegalStateException: illegal state) 56 / 87
  57. 57. 13. IO#failed (returns a failed projection). sealed trait IO[+A] { def failed: IO[Throwable] = Failed(this) } object IO { // ADT sub types private case class Failed[A](io: IO[A]) extends IO[Throwable] { override def run(): Throwable = try { io.run() throw new NoSuchElementException("failed") } catch { case nse: NoSuchElementException if nse.getMessage == "failed" => throw nse case throwable: Throwable => throwable } } } 57 / 87
  58. 58. 13. IO#failed (returns a failed projection). sealed trait IO[+A] { def failed: IO[Throwable] = Failed(this) } object IO { // ADT sub types private case class Failed[A](io: IO[A]) extends IO[Throwable] { override def run(): Throwable = try { io.run() throw new NoSuchElementException("failed") } catch { case nse: NoSuchElementException if nse.getMessage == "failed" => throw nse case throwable: Throwable => throwable } } } ADT sub type Failed wraps the IO to project. 58 / 87
  59. 59. Using IO#failed val ioError: IO[Int] = IO.raiseError[Int]( new IllegalStateException("illegal state")) println(ioError.runToEither) //=> Left(java.lang.IllegalStateException: illegal state) val failed: IO[Throwable] = ioError.failed println(failed.runToEither) //=> Right(java.lang.IllegalStateException: illegal state) val ioSuccess = IO.pure(5) println(ioSuccess.runToEither) //=> Right(5) println(ioSuccess.failed.runToEither) //=> Left(java.util.NoSuchElementException: failed) 59 / 87
  60. 60. Using IO#failed val ioError: IO[Int] = IO.raiseError[Int]( new IllegalStateException("illegal state")) println(ioError.runToEither) //=> Left(java.lang.IllegalStateException: illegal state) val failed: IO[Throwable] = ioError.failed println(failed.runToEither) //=> Right(java.lang.IllegalStateException: illegal state) val ioSuccess = IO.pure(5) println(ioSuccess.runToEither) //=> Right(5) println(ioSuccess.failed.runToEither) //=> Left(java.util.NoSuchElementException: failed) The failed projection is an IO holding a value of type Throwable, emitting the error yielded by the source, in case the source fails, otherwise if the source succeeds the result will fail with a NoSuchElementException. 60 / 87
  61. 61. 14. Other example: pure computations def sumIO(from: Int, to: Int): IO[Int] = IO { sumOfRange(from, to) } def fibonacciIO(num: Int): IO[BigInt] = IO { fibonacci(num) } def factorialIO(num: Int): IO[BigInt] = IO { factorial(num) } def computeIO(from: Int, to: Int): IO[BigInt] = for { x <- sumIO(from, to) y <- fibonacciIO(x) z <- factorialIO(y.intValue) } yield z val io: IO[BigInt] = computeIO(1, 4) implicit val ec: ExecutionContext = ExecutionContext.global io foreach { result => println(s"result = $result") } //=> 6227020800 61 / 87
  62. 62. 15. Monad instance for IO sealed trait IO[+A] { def flatMap[B](f: A => IO[B]): IO[B] = FlatMap(this, f) def map[B](f: A => B): IO[B] = flatMap(a => pure(f(a))) } object IO { def pure[A](a: A): IO[A] = Pure { () => a } implicit val ioMonad: Monad[IO] = new Monad[IO] { override def pure[A](value: A): IO[A] = IO.pure(value) override def flatMap[A, B](fa: IO[A])(f: A => IO[B]): IO[B] = fa flatMap f } } 62 / 87
  63. 63. 15. Monad instance for IO sealed trait IO[+A] { def flatMap[B](f: A => IO[B]): IO[B] = FlatMap(this, f) def map[B](f: A => B): IO[B] = flatMap(a => pure(f(a))) } object IO { def pure[A](a: A): IO[A] = Pure { () => a } implicit val ioMonad: Monad[IO] = new Monad[IO] { override def pure[A](value: A): IO[A] = IO.pure(value) override def flatMap[A, B](fa: IO[A])(f: A => IO[B]): IO[B] = fa flatMap f } } Monad instance defined in companion object (implicit scope) 63 / 87
  64. 64. Computations that abstract over HKT: F[_]: Monad import scala.language.higherKinds import cats.syntax.flatMap._ import cats.syntax.functor._ def sumF[F[_]: Monad](from: Int, to: Int): F[Int] = Monad[F].pure { sumOfRange(from, to) } def fibonacciF[F[_]: Monad](num: Int): F[BigInt] = Monad[F].pure { fibonacci(num) } def factorialF[F[_]: Monad](num: Int): F[BigInt] = Monad[F].pure { factorial(num) } def computeF[F[_]: Monad](from: Int, to: Int): F[BigInt] = for { x <- sumF(from, to) y <- fibonacciF(x) z <- factorialF(y.intValue) } yield z This code can be used with IO or any other Monad. 64 / 87
  65. 65. Reify F[_] : MonadF[_] : Monad with IOIO import scala.concurrent.ExecutionContext.Implicits.global val io: IO[BigInt] = computeF[IO](1, 4) io foreach { result => println(s"result = $result") } //=> 6227020800 65 / 87
  66. 66. Reify F[_] : MonadF[_] : Monad with IOIO import scala.concurrent.ExecutionContext.Implicits.global val io: IO[BigInt] = computeF[IO](1, 4) io foreach { result => println(s"result = $result") } //=> 6227020800 Reify F[_] : MonadF[_] : Monad with cats.Idcats.Id val result: cats.Id[BigInt] = computeF[cats.Id](1, 4) println(s"result = $result") //=> 6227020800 66 / 87
  67. 67. Reify F[_] : MonadF[_] : Monad with IOIO import scala.concurrent.ExecutionContext.Implicits.global val io: IO[BigInt] = computeF[IO](1, 4) io foreach { result => println(s"result = $result") } //=> 6227020800 Reify F[_] : MonadF[_] : Monad with cats.Idcats.Id val result: cats.Id[BigInt] = computeF[cats.Id](1, 4) println(s"result = $result") //=> 6227020800 Reify F[_] : MonadF[_] : Monad with OptionOption import cats.instances.option._ val maybeResult: Option[BigInt] = computeF[Option](1, 4) maybeResult foreach { result => println(s"result = $result") } //=> 6227020800 67 / 87
  68. 68. Reify F[_] : MonadF[_] : Monad with IOIO import scala.concurrent.ExecutionContext.Implicits.global val io: IO[BigInt] = computeF[IO](1, 4) io foreach { result => println(s"result = $result") } //=> 6227020800 Reify F[_] : MonadF[_] : Monad with cats.Idcats.Id val result: cats.Id[BigInt] = computeF[cats.Id](1, 4) println(s"result = $result") //=> 6227020800 Reify F[_] : MonadF[_] : Monad with OptionOption import cats.instances.option._ val maybeResult: Option[BigInt] = computeF[Option](1, 4) maybeResult foreach { result => println(s"result = $result") } //=> 6227020800 Reify F[_] : MonadF[_] : Monad with FutureFuture import scala.concurrent.{Future, ExecutionContext} import ExecutionContext.Implicits.global import cats.instances.future._ val future: Future[BigInt] = computeF[Future](1, 4) future foreach { result => println(s"result = $result") } //=> 6227020800 68 / 87
  69. 69. 16. IO.defer and IO.suspend object IO { // ADT sub types private case class Suspend[A](thunk: () => IO[A]) extends IO[A] { override def run(): A = thunk().run() } def suspend[A](ioa: => IO[A]): IO[A] = Suspend(() => ioa) def defer[A](ioa: => IO[A]): IO[A] = suspend(ioa) } 69 / 87
  70. 70. 16. IO.defer and IO.suspend object IO { // ADT sub types private case class Suspend[A](thunk: () => IO[A]) extends IO[A] { override def run(): A = thunk().run() } def suspend[A](ioa: => IO[A]): IO[A] = Suspend(() => ioa) def defer[A](ioa: => IO[A]): IO[A] = suspend(ioa) } These methods defer the (possibly immediate) side effect of the inner IO. ADT sub type Suspend wraps another IO. 70 / 87
  71. 71. Using IO.defer IO.pure(...) // without IO.defer val io1 = IO.pure { println("immediate side effect"); 5 } //=> immediate side effect Thread sleep 2000L io1 foreach println //=> 5 71 / 87
  72. 72. Using IO.defer IO.pure(...) // without IO.defer val io1 = IO.pure { println("immediate side effect"); 5 } //=> immediate side effect Thread sleep 2000L io1 foreach println //=> 5 IO.defer(IO.pure(...)) val io2 = IO.defer { IO.pure { println("deferred side effect"); 5 } } Thread sleep 2000L io2 foreach println //=> deferred side effect //=> 5 72 / 87
  73. 73. 17. IO.fromTry and IO.fromEither object IO { def fromTry[A](tryy: Try[A]): IO[A] = tryy.fold(IO.raiseError, IO.pure) def fromEither[A](either: Either[Throwable, A]): IO[A] = either.fold(IO.raiseError, IO.pure) } 73 / 87
  74. 74. 17. IO.fromTry and IO.fromEither object IO { def fromTry[A](tryy: Try[A]): IO[A] = tryy.fold(IO.raiseError, IO.pure) def fromEither[A](either: Either[Throwable, A]): IO[A] = either.fold(IO.raiseError, IO.pure) } val tryy: Try[Seq[User]] = Try { User.getUsers } val io1: IO[Seq[User]] = IO.fromTry(tryy) val either: Either[Throwable, Seq[User]] = tryy.toEither val io2: IO[Seq[User]] = IO.fromEither(either) 74 / 87
  75. 75. 18. IO.fromFuture object IO { // ADT sub types private case class FromFuture[A](fa: Future[A]) extends IO[A] { override def run(): A = Await.result(fa, Duration.Inf) // BLOCKING!!! } def fromFuture[A](future: Future[A]): IO[A] = FromFuture(future) } 75 / 87
  76. 76. 18. IO.fromFuture object IO { // ADT sub types private case class FromFuture[A](fa: Future[A]) extends IO[A] { override def run(): A = Await.result(fa, Duration.Inf) // BLOCKING!!! } def fromFuture[A](future: Future[A]): IO[A] = FromFuture(future) } Attention: The implementation of fromFuture is a bit simplistic. Waiting for the Future to complete might block a thread! (A solution of this problem would require a redesign of my simple IO Monad, which doesn't support non-blocking async computations.) 76 / 87
  77. 77. Using IO.fromFuture def futureGetUsers(implicit ec: ExecutionContext): Future[Seq[User]] = Future { println("side effect") User.getUsers } 77 / 87
  78. 78. Using IO.fromFuture def futureGetUsers(implicit ec: ExecutionContext): Future[Seq[User]] = Future { println("side effect") User.getUsers } IO.fromFuture(f) is eager. implicit val ec: ExecutionContext = ExecutionContext.global val io = IO.fromFuture { futureGetUsers } io foreach { users => users foreach println } //=> "side effect" io foreach { users => users foreach println } 78 / 87
  79. 79. Using IO.fromFuture def futureGetUsers(implicit ec: ExecutionContext): Future[Seq[User]] = Future { println("side effect") User.getUsers } IO.fromFuture(f) is eager. implicit val ec: ExecutionContext = ExecutionContext.global val io = IO.fromFuture { futureGetUsers } io foreach { users => users foreach println } //=> "side effect" io foreach { users => users foreach println } IO.defer(IO.fromFuture(f)) is lazy. implicit val ec: ExecutionContext = ExecutionContext.global val io = IO.defer { IO.fromFuture { futureGetUsers } } io foreach { users => users foreach println } //=> "side effect" io foreach { users => users foreach println } //=> "side effect" 79 / 87
  80. 80. 19. IO.deferFuture object IO { def deferFuture[A](fa: => Future[A]): IO[A] = defer(IO.fromFuture(fa)) } 80 / 87
  81. 81. 19. IO.deferFuture object IO { def deferFuture[A](fa: => Future[A]): IO[A] = defer(IO.fromFuture(fa)) } IO.deferFuture(f) is an alias for IO.defer(IO.fromFuture(f)). An ExecutionContext is still required to create the Future! 81 / 87
  82. 82. Using IO.deferFuture def futureGetUsers(implicit ec: ExecutionContext): Future[Seq[User]] = Future { println("side effect") User.getUsers } 82 / 87
  83. 83. Using IO.deferFuture def futureGetUsers(implicit ec: ExecutionContext): Future[Seq[User]] = Future { println("side effect") User.getUsers } IO.defer(IO.fromFuture(f)) is lazy. implicit val ec: ExecutionContext = ExecutionContext.global val io = IO.defer { IO.fromFuture { futureGetUsers } } io foreach { users => users foreach println } //=> "side effect" io foreach { users => users foreach println } //=> "side effect" 83 / 87
  84. 84. Using IO.deferFuture def futureGetUsers(implicit ec: ExecutionContext): Future[Seq[User]] = Future { println("side effect") User.getUsers } IO.defer(IO.fromFuture(f)) is lazy. implicit val ec: ExecutionContext = ExecutionContext.global val io = IO.defer { IO.fromFuture { futureGetUsers } } io foreach { users => users foreach println } //=> "side effect" io foreach { users => users foreach println } //=> "side effect" IO.deferFuture(f) is a shortcut for that. implicit val ec: ExecutionContext = ExecutionContext.global val io = IO.deferFuture { futureGetUsers } io foreach { users => users foreach println } //=> "side effect" io foreach { users => users foreach println } //=> "side effect" 84 / 87
  85. 85. 4. Resources 85 / 87
  86. 86. Resources Code and Slides of this Talk: https://github.com/hermannhueck/implementing-io-monad Code and Slides for my Talk on: Future vs. Monix Task https://github.com/hermannhueck/future-vs-monix-task Monix Task 3.x Documentation (for comparison with IO) https://monix.io/docs/3x/eval/task.html Monix Task 3.x API Documentation (for comparison with IO) https://monix.io/api/3.0/monix/eval/Task.html Best Practice: "Should Not Block Threads" https://monix.io/docs/3x/best-practices/blocking.html What Referential Transparency can do for you Talk by Luka Jacobowitz at ScalaIO 2017 https://www.youtube.com/watch?v=X-cEGEJMx_4 86 / 87
  87. 87. Thanks for Listening Q & A https://github.com/hermannhueck/implementing-io-monad 87 / 87

×