Wednesday, July 8, 2020

ZIO cheats


Here are some brief notes I made to help myself become more familiar with ZIO.

The ZIO Monad

The ZIO monad is defined as ZIO[-R, +E, +A] where R is the requirement, the E is the error and the A is the value.

To feed the requirements into ZIO, use the compose function, its >>> synonym or a provideXXX function.


Useful type aliases

A U in ZIO type names indicates that some code cannot error. For instance:

UIO[+A]      = ZIO[Any, Nothing, A]
URIO[-R, +A] = ZIO[R,   Nothing, A]

Mnemonic: think of that U as an indication the monad is Unexceptional.

Note that you can't have a reference to Nothing. For example:

Welcome to the Ammonite Repl 2.0.4 (Scala 2.13.1 Java 1.8.0_241)
@ val x: Nothing = "Hello" 
cmd0.sc:1: type mismatch;
 found   : String("Hello")
 required: Nothing
val x: Nothing = "Hello"

Since something can never be Nothing, we know that branch of the code cannot be executed. 


Error handling

... is perhaps simpler in ZIO when compared to Cats. You simply call catchAll on your ZIO monad (see this article for fallback, folding and retries).

If we want to ignore an exception then you can do something like:

    val result:     ZIO[Any, DBError, Unit] = makeUser.provideLayer(fullLayer)
    val exit:       UIO[Int]                = UIO(1)   
    val resultExit: ZIO[Any, DBError, Int]  = result *> exit
    val afterCatch: ZIO[Any, Nothing, Int]  = resultExit.catchAll(_ => exit)

Obviously, this won't compile:

  override def run(args: List[String]): ZIO[zio.ZEnv, Nothing, Int] = {
    val exitResult: ZIO[Any, DBError, Unit] = exit *> result
    exitResult.catchAll(_ => exit)
  }

as we've only changed the error channel type but the value value is still Unit.

If you want to only take the successful results, you need something like:

  val xs:        List[IO[Throwable, String]] = ???
  val successes: UIO[List[String]]           = ZIO.collectAllSuccesses(xs)

One way to get you hands on an error is using ZIO.flip which can be useful in testing [SO].


Combinators

The joke in the Cats world is that the answer is always Traverse. But in ZIO, this operation is collectAll (or collectAllPar if you want the work done in parrallel). It's a pity that that ZIO didn't call it Sequence (a close relative of Traverse) as this has precedent in the plain old Scala world in Future.sequence.

jdegoes 12 April 2020 at 2:10 PM (Discord)
x1.ignore &> x2.ignore &> x3. This will execute x1x2, and x3 in parallel, using zipRightPar  (that's the &> operator, zips two effects together in parallel, returning whatever is produced on the right), and ignore the result of x1 and x2 so their failures don't influence the result of the computation.
 
In ZIO, even parallel zip or collect or foreach operations will "kill" the other running effects if one of them fails. Because that's often what you want. To get the other behavior, just use .ignore in the right places to ignore the failures you don't care about.


Threads

ZIO effects can be executed on other Scala ExecutionContexts with the use of ZIO.on.

Alternatively, you might like to use zio.blocking.XXX which "provides access to a thread pool that can be used for performing blocking operations, such as thread sleeps, synchronous socket/file reads, and so forth. The contract is that the thread pool will accept unlimited tasks (up to the available memory) and continuously create new threads as necessary." [JavaDoc]

For the purposes of cancellation: 

adamfraser 25/6/2020 at 11:13 PM (Discord)
Interruption is checked between each flatMap.

If for whatever reason you can't run your code in a zio.App, you can call zio.Runtime.default.unsafeRun on your effect.


Interop with plain Scala

ZIO maps nicely to plain Scala classes. For instance, ZIO[?, E, A].either yields a ZIO that wraps a plain, old Scala Either[E, A]. Going the other way is simply a ZIO.fromEither call.

Similarly, .option will yield a ZIO that wraps a None or Some[A].


Resource management

ZIO resourcement management is syntactically similar to Cats where bracket takes the code to close the resource and the code that uses the resource:

  val toInputStream: Task[InputStream] = ...

  toInputStream(resource).bracket(close(_)) { input =>
    ZIO {
       new FileOutputStream(output)
    }.bracket(close(_)) { output =>
      ZIO { IOUtils.pipe(input, output) }
    }


Testing

If you want ZIO tests to be picked up as part of a build that uses JUnit then your class should extend zio.test.junit.JUnitRunnableSpec.

You can fail tests on timeout with:

import zio.test.TestAspect._
import zio.duration._

  override def spec: ZSpec[TestEnvironment, Any] = suite("...")(testM("..."){
...
  } @@ timeout(10 seconds)



No comments:

Post a Comment