Effects

A functional programming style promotes explicit and thoughtful management of side effects. A Stream is built using composition and mutability. Side effects are delayed until the stream is fully composed and assembled into its final context. Then the context is run based on the API that your context type provides. This follows the typical functional style of building your computation and isolating the parts of your code that contain side effects.

fs2 isolates side effects by using the F parameter in Stream functions. The F may be consulted when running the stream.

F must be "evaluated" based on the API of the context you are using. In scala, anytime a function returns a Unit it is equivalent to a java method returning void. A return value of Unit usually indicates that a function is not a pure function and the function exists for its side effects.

By allowing a context for side effects and parameterizing the Stream type on the context, you can use the context of your choice. fs2 provides a general purpose Task environment that is a very common choice for the context when using fs2. A Task can be run be run synchronously or asynchronously. Task is really a simple wrapper around some code that may return a value or Unit. The wrapper allows the function to be evaluated differently based on your application needs. A context can also be a standard scala Future or some other Monad type but then you must provide some type classes for your context type. Most users will use Task and fs2 provides all the support for Task that you need.

In fs2, side effects are monads with a variety of additional behaviors including:

  • Catchable - provide behaviours for managing exceptions

  • Suspendable - The ability to obtain a context (with a value inside) each time it is asked for. Essentially, this is like a by-name parameter in scala.

  • Running asynchronously - since evaluating an effect is unsafe, because it could throw an exception or interact with the outside world, running an effect has a method called unsafeRunAsync.

Task itself is independent of streams, its a separate concept. It's useful in fs2 so that fs2 can provide a sturdy, useful effects container for your programs. Scala's future suffers from a few issues including the inability to control when the future starts its computations and its simple representation of a failed computation using exceptions.

fs2's Task provides a wide variety of ways to run it. Generally, to understand the methods you can use, you need to understand that there are two core concepts:

  • Choose whether to run the Task asynchronously or synchronously. If running synchronously, you may or may not be able to specify a timeout to wait for Task completion before it signals an error.

  • Choose how you want to represent success and failure. Future's represent failure using an exception. You can also throw an exception or use another convenient monad.

So let's say you can capture success and failure by throwing an exception, using a Future, an Option (but an option does not carry any real information about the failure only that a value was not returned), a Try or an Either (left is bad, right is good). That's 5 different ways to record "failure" with varying degrees of usefulness. If there are 5 ways to capture success/failure and 2 ways to run the task, that's roughly 10 different ways to "run" a task. On the javascript platform, you cannot block so you cannot as for an asynchronous run then block to wait for the result so some Task unsafeRun type methods are unavailable.

Last updated