Creating Effects
This section explores some of the common ways to create ZIO effects from values, from common Scala types, and from both synchronous and asynchronous side-effects.
From Success Valuesβ
Using the ZIO.succeed
method, you can create an effect that succeeds with the specified value:
val s1 = ZIO.succeed(42)
You can also use methods in the companion objects of the ZIO
type aliases:
val s2: Task[Int] = Task.succeed(42)
The succeed
method takes a by-name parameter to make sure that any accidental side effects from constructing the value can be properly managed by the ZIO Runtime. However, succeed
is intended for values which do not have any side effects. If you know that your value does have side effects consider using ZIO.effectTotal
for clarity.
val now = ZIO.effectTotal(System.currentTimeMillis())
The value inside a successful effect constructed with ZIO.effectTotal
will only be constructed if absolutely required.
From Failure Valuesβ
Using the ZIO.fail
method, you can create an effect that models failure:
val f1 = ZIO.fail("Uh oh!")
For the ZIO
data type, there is no restriction on the error type. You may use strings, exceptions, or custom data types appropriate for your application.
Many applications will model failures with classes that extend Throwable
or Exception
:
val f2 = Task.fail(new Exception("Uh oh!"))
Note that unlike the other effect companion objects, the UIO
companion object does not have UIO.fail
, because UIO
values cannot fail.
From Scala Valuesβ
Scala's standard library contains a number of data types that can be converted into ZIO effects.
Optionβ
An Option
can be converted into a ZIO effect using ZIO.fromOption
:
val zoption: IO[Option[Nothing], Int] = ZIO.fromOption(Some(2))
The error type of the resulting effect is Option[Nothing]
, which provides no information on why the value is not there. You can change the Option[Nothing]
into a more specific error type using ZIO#mapError
:
val zoption2: IO[String, Int] = zoption.mapError(_ => "It wasn't there!")
You can also readily compose it with other operators while preserving the optional nature of the result (similar to an OptionT
)
val maybeId: IO[Option[Nothing], String] = ZIO.fromOption(Some("abc123"))
def getUser(userId: String): IO[Throwable, Option[User]] = ???
def getTeam(teamId: String): IO[Throwable, Team] = ???
val result: IO[Throwable, Option[(User, Team)]] = (for {
id <- maybeId
user <- getUser(id).some
team <- getTeam(user.teamId).asSomeError
} yield (user, team)).optional
Eitherβ
An Either
can be converted into a ZIO effect using ZIO.fromEither
:
val zeither = ZIO.fromEither(Right("Success!"))
The error type of the resulting effect will be whatever type the Left
case has, while the success type will be whatever type the Right
case has.
Tryβ
A Try
value can be converted into a ZIO effect using ZIO.fromTry
:
import scala.util.Try
val ztry = ZIO.fromTry(Try(42 / 0))
The error type of the resulting effect will always be Throwable
, because Try
can only fail with values of type Throwable
.
Functionβ
A function A => B
can be converted into a ZIO effect with ZIO.fromFunction
:
val zfun: URIO[Int, Int] =
ZIO.fromFunction((i: Int) => i * i)
The environment type of the effect is A
(the input type of the function), because in order to run the effect, it must be supplied with a value of this type.
Futureβ
A Future
can be converted into a ZIO effect using ZIO.fromFuture
:
import scala.concurrent.Future
lazy val future = Future.successful("Hello!")
val zfuture: Task[String] =
ZIO.fromFuture { implicit ec =>
future.map(_ => "Goodbye!")
}
The function passed to fromFuture
is passed an ExecutionContext
, which allows ZIO to manage where the Future
runs (of course, you can ignore this ExecutionContext
).
The error type of the resulting effect will always be Throwable
, because Future
can only fail with values of type Throwable
.
From Side-Effectsβ
ZIO can convert both synchronous and asynchronous side-effects into ZIO effects (pure values).
These functions can be used to wrap procedural code, allowing you to seamlessly use all features of ZIO with legacy Scala and Java code, as well as third-party libraries.
Synchronous Side-Effectsβ
A synchronous side-effect can be converted into a ZIO effect using ZIO.effect
:
import scala.io.StdIn
val getStrLn: Task[String] =
ZIO.effect(StdIn.readLine())
The error type of the resulting effect will always be Throwable
, because side-effects may throw exceptions with any value of type Throwable
.
If a given side-effect is known to not throw any exceptions, then the side-effect can be converted into a ZIO effect using ZIO.effectTotal
:
def putStrLn(line: String): UIO[Unit] =
ZIO.effectTotal(println(line))
You should be careful when using ZIO.effectTotal
βwhen in doubt about whether or not a side-effect is total, prefer ZIO.effect
to convert the effect.
If you wish to refine the error type of an effect (by treating other errors as fatal), then you can use the ZIO#refineToOrDie
method:
import java.io.IOException
val getStrLn2: IO[IOException, String] =
ZIO.effect(StdIn.readLine()).refineToOrDie[IOException]
Asynchronous Side-Effectsβ
An asynchronous side-effect with a callback-based API can be converted into a ZIO effect using ZIO.effectAsync
:
object legacy {
def login(
onSuccess: User => Unit,
onFailure: AuthError => Unit): Unit = ???
}
val login: IO[AuthError, User] =
IO.effectAsync[AuthError, User] { callback =>
legacy.login(
user => callback(IO.succeed(user)),
err => callback(IO.fail(err))
)
}
Asynchronous ZIO effects are much easier to use than callback-based APIs, and they benefit from ZIO features like interruption, resource-safety, and superior error handling.
Blocking Synchronous Side-Effectsβ
Some side-effects use blocking IO or otherwise put a thread into a waiting state. If not carefully managed, these side-effects can deplete threads from your application's main thread pool, resulting in work starvation.
ZIO provides the zio.blocking
package, which can be used to safely convert such blocking side-effects into ZIO effects.
A blocking side-effect can be converted directly into a ZIO effect blocking with the effectBlocking
method:
import zio.blocking._
val sleeping =
effectBlocking(Thread.sleep(Long.MaxValue))
The resulting effect will be executed on a separate thread pool designed specifically for blocking effects.
Blocking side-effects can be interrupted by invoking Thread.interrupt
using the effectBlockingInterrupt
method.
Some blocking side-effects can only be interrupted by invoking a cancellation effect. You can convert these side-effects using the effectBlockingCancelable
method:
import java.net.ServerSocket
import zio.UIO
def accept(l: ServerSocket) =
effectBlockingCancelable(l.accept())(UIO.effectTotal(l.close()))
If a side-effect has already been converted into a ZIO effect, then instead of effectBlocking
, the blocking
method can be used to ensure the effect will be executed on the blocking thread pool:
import scala.io.{ Codec, Source }
def download(url: String) =
Task.effect {
Source.fromURL(url)(Codec.UTF8).mkString
}
def safeDownload(url: String) =
blocking(download(url))
Next Stepsβ
If you are comfortable creating effects from values, Scala data types, and side-effects, the next step is learning basic operations on effects.