Category Theory for fs2

fs2 uses design patterns that are important to recognize. Knowing the patterns will help you create and debug your programs. The design patterns described in this section are those from category theory.

Monads Abound

fs2 relies on the Monad design pattern to allow the library to be as independent as possible of specific representations of your data. As long as your context F is a Monad, fs2 can map and flatMap into it generically to apply functions to the value or perform some type of binary associative operation, for example "append", to perform stream processing.

Stream is a Monad

The process type Stream is a monad. This means that you can flatMap it and map it. Stream being a Monad means that you need to use the model of lifting your domain functions into the Stream context so that your function is applied to the value.

The fact that a Stream is a monad is actually critical. You may have used monad's in other applications e.g. List or Future. You saw that you build up your computations on a monad by using map/flatMap. Often, you were mapping into a monad and transforming a single value into another value.

In fs2 Streams, mapping and flatmapping into a stream produces another stream and it is more frequently the case that the initial stream may only hold a single value but your mapping function may produce an infinite number of values. In this model, the initial one-element monad contains a resource that is used to generate a stream with more than one value. For example:

Stream(yourresource).flatMap(res => create_infinite_stream(res))...

Some of the more useful objects in fs2 are often used this way. For example, you can create a signal using

Stream.eval(async.signalOf[Task](value)).flatMap(signal => ... )...

Here, eval just evaluates the Task wrapping that wraps the signalOf's return value then flatMap into it to use that "resource."

Coproducts

If you have two object types, A and B, you can form a Coproduct using Either. This allows you to break a single stream into a stream of two element types--well this is almost true. fs2's model is pull-based. The implication is that fan-in's are easy but fan-outs are hard. A two type fan-out can be simulated by using a Coproduct to represent the two types. A Coprodut is like an union type but with tags so you know what type the object is inside. You can generalize a Coproduct to join together many different types. In reality, your process is acting on the Coproduct. By writing processes that only operate on a specific "tag" of the Coproduct, say the left side, the process appears to be operating on only one part of the "fan-out". The process would pass through any other type unprocessed. If you were to then compose a series of procesess, each operating on only one type and passing through the rest, it would appear that your stream is performing a fan-out.

A Coproduct is a general concept. You can generalize a simple Either to a shapeless-style product that can hold more than just two values.

There are limitations to the types of fan-out processing you can perform. In some cases, you need a fan-out to process data along different paths with different end-points. In this scenario, you really have two streams that sources from the same upstream stream. You can identify when you need to have two streams if you have two different processes that you call run on to create your stream.

Free Monad

fs2 uses a built-in interpreter to interpret the steps that must be taken to process the items in a stream. The Free monad allows instructions to be built up then run to process a stream. Free monad's are advanced topic and not critical to using fs2 so we'll skip any further exposition on free monad's in this book.

Last updated