IO
is the most common data type used to represent side-effects in functional languages.
This means IO
is the data type of choice when interacting with the external environment: databases, network, operative systems, files…
IO
is used to represent operations that can be executed lazily and are capable of failing, generally with exceptions.
This means that code wrapped inside IO
will not throw exceptions until it is run, and those exceptions can be captured inside IO
for the user to check.
The first challenge for someone new to effects withIO
is evaluating its result. Given that IO
is used to wrap operations with the environment,
the return value after completion is commonly used in another part of the program.
Coming from an OOP background the simplest way to use the return value of IO
is to consider it as an asynchronous operation you can register a callback for.
IO
objects can be constructed passing them functions that will not be immediately called. This is called lazy evaluation.
Running in this context means evaluating the content of an IO
object, and propagating its result in a synchronous, asynchronous, or deferred way.
Note that IO
objects can be run multiple times, and depending on how they are constructed they will evaluate its content again every time they’re run.
Executes and defers the result into a new IO
that has captured any exceptions inside Either<Throwable, A>
.
Running this new IO
will work over its result, rather than on the original IO
content where attempt()
was called on.
IO<Int> { throw RuntimeException() }
.attempt()
Takes as a parameter a callback from a result of Either<Throwable, A>
to a new IO<Unit>
instance.
All exceptions that would happen on the function parameter are automatically captured and propagated to the IO<Unit>
return.
It runs the current IO
asynchronously, calling the callback parameter on completion and returning its result.
IO<Int> { throw RuntimeException("Boom!") }
.runAsync { result ->
result.fold({ IO { println("Error") } }, { IO { println(it.toString()) } })
}
Takes as a parameter a callback from a result of Either<Throwable, A>
.
This callback is assumed to never throw any internal exceptions.
It runs the current IO
asynchronously, calling the callback parameter on completion.
IO<Int> { throw RuntimeException("Boom!") }
.unsafeRunAsync { result ->
result.fold({ println("Error") }, { println(it.toString()) })
}
To be used with SEVERE CAUTION, it runs IO
synchronously and returns an Option<A>
blocking the current thread. It requires a timeout parameter.
If the any non-blocking operation performed inside IO
lasts longer than the timeout, unsafeRunSyncTimed
returns None
.
If your program has crashed, this function call is a good suspect. To avoid crashing use attempt()
first.
If your multithreaded program deadlocks, this function call is a good suspect.
If your multithreaded program halts and never completes, this function call is a good suspect.
IO<Int> { throw RuntimeException("Boom!") }
.attempt()
.unsafeRunTimed(100.milliseconds)
IO.async<Int> { }
.attempt()
.unsafeRunTimed(100.milliseconds)
To be used with SEVERE CAUTION, it runs IO
synchronously and returning its result blocking the current thread.
It generally should be used only for examples & tests.
If your program has crashed, this function call is a good suspect. To avoid crashing use attempt()
first.
If your multithreaded program deadlocks, this function call is a good suspect.
If your multithreaded program halts and never completes, this function call is a good suspect.
IO<Int> { throw RuntimeException("Boom!") }
.attempt()
.unsafeRunSync()
IO { 1 }
.attempt()
.unsafeRunSync()
As we have seen above, the way of constructing an IO
affects its behavior when run multiple times.
Understanding the constructors is key to mastering IO
.
Used to wrap single values. It creates anIO
that returns an existing value.
IO.just(1)
.unsafeRunSync()
Used to notify of errors during execution. It creates an IO
that returns an existing exception.
IO.raiseError<Int>(RuntimeException("Boom!"))
.attempt()
.unsafeRunSync()
Generally used to wrap existing blocking functions. Creates an IO
that invokes one lambda function when run.
Note that this function is evaluated every timeIO
is run.
IO { 1 }
.unsafeRunSync()
IO<Int> { throw RuntimeException("Boom!") }
.attempt()
.unsafeRunSync()
Commonly used to aggregate the results of multiple independent blocking functions. Creates an IO
that invokes 2-10 functions when run. Their results are accumulated on a TupleN, where N is the size.
IO.merge ({ 1 }, { 2 }, { 3 })
.attempt()
.unsafeRunSync()
IO.merge ({ 1 }, { 2 }, { throw RuntimeException("Boom!") })
.attempt()
.unsafeRunSync()
Used to defer the evaluation of an existing IO
.
IO.suspend { IO.just(1) }
.attempt()
.unsafeRunSync()
Mainly used to integrate with existing frameworks that have asynchronous calls.
It requires a function that provides a callback parameter and it expects for the user to start an operation using the other framework.
The callback parameter has to be invoked with an Either<Throwable, A>
once the other framework has completed its execution.
Note that if the callback is never called IO will run forever and not terminate unless run using unsafeRunTimed()
.
IO.async<Int> { callback ->
callback(1.right())
}
.attempt()
.unsafeRunSync()
IO.async<Int> { callback ->
callback(RuntimeException("Boom").left())
}
.attempt()
.unsafeRunSync()
IO
is usually best paired with comprehensions to get a cleaner syntax.
Comprehensions also enable cancellation and parallelization of IO effects.
IO.monad().binding {
val file = getFile("/tmp/file.txt").bind()
val lines = file.readLines().bind()
val average =
if (lines.isEmpty()) {
0
} else {
val count = lines.map { it.length }.foldLeft(0) { acc, lineLength -> acc + lineLength }
count / lines.length
}
average
}
.fix()
.attempt()
.unsafeRunSync()
Puts the value A
inside an IO<A>
using just
.
1.liftIO()
.attempt()
.unsafeRunSync()
IO implements all the operators common to all instances of MonadError
. Those include map
, flatMap
, and handleErrorWith
.