{{announcement.body}}
{{announcement.title}}

Compose Cats, Reactor, and ZIO — Effects

DZone 's Guide to

Compose Cats, Reactor, and ZIO — Effects

In this article, take a look at the type system for Inversion of Coupling Control to provide composition.

· Java Zone ·
Free Resource

This is the third in a series of articles looking at the type system for Inversion of Coupling Control to provide composition.

The previous articles covered:

This article will look at taking the theory into practice. It will use the concepts to build an application composing Effects from various Effect libraries.

Note that the Effects used are kept deliberately simple to focus on the composition of the effects. This is mainly because this article is not to compare libraries. This article is to compose them. We show how using Inversion of Coupling Control, they can be seamlessly composed together in a simple application. Also, the order of discussing the libraries is nothing more than alphabetical.

To keep matters simple, the effect will be retrieving a message from the database.

You might also like:  ZIO and Cats Effect: Reactive Streams for Functional Scala

Cats

Let's begin with the Cats Effect.

Scala
 




xxxxxxxxxx
1


 
1
  def cats(request: ServerRequest)(implicit repository: MessageRepository): IO[ServerResponse] =
2
    for {
3
      message <- catsGetMessage(request.getId)
4
      response = new ServerResponse(s"${message.getContent} via Cats")
5
    } yield response
6
 
          
7
  def catsGetMessage(id: Int)(implicit repository: MessageRepository): IO[Message] =
8
    IO.apply(repository findById id orElseThrow)



The catsGetMessage  function wraps the repository retrieving message effect within an IOThis can then be used to service the request to provide a response (as per the cats  function).

The use of implicit  may be overkill for the single repository dependency. However, it shows how dependency injection can remove dependency clutter from the servicing logic. This is especially useful when the number of dependencies grows.

Reactor

Reactor has the following servicing logic.

Scala
 




xxxxxxxxxx
1


 
1
  def reactor(request: ServerRequest)(implicit repository: MessageRepository): Mono[ServerResponse] =
2
    reactorGetMessage(request.getId).map(message => new ServerResponse(s"${message.getContent} via Reactor"))
3
 
          
4
  def reactorGetMessage(id: Int)(implicit repository: MessageRepository): Mono[Message] =
5
    Mono.fromCallable(() => repository.findById(id).orElseThrow())



Again, there is a reactorGetMessage  function wrapping the retrieving message effect into a Mono . This is then used to service the request.

ZIO

For ZIO the logic is slightly different, as ZIO provides its own dependency injection.

Scala
 




xxxxxxxxxx
1
19


 
1
  def zio(request: ServerRequest, repository: MessageRepository): ZIO[Any, Throwable, ServerResponse] = {
2
    // Service logic
3
    val response = for {
4
      message <- zioGetMessage(request.getId)
5
      response = new ServerResponse(s"${message.getContent} via ZIO")
6
    } yield response
7
 
          
8
    // Provide dependencies
9
    response.provide(new InjectMessageRepository {
10
      override val messageRepository = repository
11
    })
12
  }
13
 
          
14
  def zioGetMessage(id: Int): ZIO[InjectMessageRepository, Throwable, Message] =
15
    ZIO.accessM(env => ZIO.effect(env.messageRepository.findById(id).orElseThrow()))
16
 
          
17
  trait InjectMessageRepository {
18
    val messageRepository: MessageRepository
19
  }



The zioGetMessage  again wraps the retrieve database message effect within a ZIO . However, it extracts the injected trait to retrieve the repository.

Encapsulating Into a Module

The above functions ( cats , reactor , zio ) are configured as First-Class Procedures into the following Module.

Module

This module has an output being the Response  with inputs Cats , Reactor , ZIO  and Imperative .

As First-Class Procedures are lazily evaluated they can also wrap imperative code containing effects. The imperative  function is the following.

Scala
 




xxxxxxxxxx
1


 
1
  def imperative(request: ServerRequest, repository: MessageRepository): ServerResponse = {
2
    val message = repository.findById(request.getId).orElseThrow()
3
    new ServerResponse(s"${message.getContent} via Imperative")
4
  }



Using the Module

The following configuration uses the module to service REST requests. It is configured as the Synchronous module.

Module

This demonstrates how easy it is to configure the module into servicing requests.

What is further interesting is the Asynchronous module has the same interface of inputs/outputs as the Synchronous module. Now, this could quite possibly be the above module re-used (just badly named). However, it is not. The Asynchronous module undertakes the same logic, but just asynchronously (code available in demo project).

What is important for modules is the contractual interface of inputs and outputs. We could quite happily swap the Synchronous / Asynchronous modules around in the configuration and the application will still continue to work. This allows the complexity to be encapsulated.

A more real-world example is we could start out with the quicker to write and easier to debug synchronous effects. Then as the application grows in scale, we may decide to swap in an asynchronous module to better handle scale. The amount of refactoring to swap the Synchronous module to the Asynchronous module would be:

  1. Drop in new Asynchronous module
  2. Re-wire flows to the Asynchronous module
  3. Delete the Synchronous module

As Inversion of Coupling Control removes the function coupling, there is no code to change except providing the implementation of the new module.

With modules able to contain modules, this provides a means to encapsulate complexity of the application for easier comprehension. It also makes importing modules simple. Drop them in and wire them up. And is especially useful when libraries of third party modules are available for composition of ready to use functionality.

Composing Effects

This demonstrates First-Class Procedures and First-Class Modules of the previous articles in this series.

Hey, but this article promised composing effects!

Well I could tell you the send is an effect and that composing this after the above effects is that composition. However, that's taking a lot of my word for it.

Therefore, the last module in the server configuration is the following.

module

This module composes an effect from each of the libraries. The code for each effect is the following.

Scala
 




xxxxxxxxxx
1
11


 
1
  def seed: String = "Hi"
2
 
          
3
  def cats(@Parameter param: String): IO[String] = IO.pure(s"$param, via Cats")
4
 
          
5
  def reactor(@Parameter param: String): Mono[String] = Mono.just(s"$param, via Reactor")
6
 
          
7
  def zio(@Parameter param: String): ZIO[Any, Nothing, String] = ZIO.succeed(s"$param, via ZIO")
8
 
          
9
  def imperative(@Parameter param: String): String = s"$param, via Imperative"
10
 
          
11
  def response(@Parameter message: String): ServerResponse = new ServerResponse(message)



Each effect just takes the input of the previous and appends it's library name. The resulting response is a string containing all the effect library names.

No Adapters

Astute readers may be thinking that under the hood of OfficeFloor there may be some fabulous adapters between the libraries. Hmmm, can we extract these and make use of them?

Sadly and for that matter quite happily there are no adapters between the libraries. What actually happens is that each First-Class Procedure unsafely executes its effect and retrieves the resulting output. With the output OfficeFloor then invokes the next First-Class Procedure. By doing so, we do not need to adapt the libraries with each other. We can run each effect in isolation and interface them via their typed inputs/outputs.

This makes integration of new effect libraries very simple. Just write a once off adapter to encapsulate the library's effects within a First-Class Procedure. The effect library is then able to integrate with all the other effect libraries. As First-Class Procedures are actually a specialised First-Class Module, this demonstrates the composition capabilities of Inversion of Coupling Control.

Summary

This article has been code and configuration heavy to demonstrate how First-Class Procedures and First-Class Modules compose.

It has demonstrated that the type system of Inversion of Coupling Control makes composition easy (essentially drawing lines).

Now you need not take my word on the code examples in this article. They are extracted from the demonstration project you can clone and run yourself (found at https://github.com/officefloor/ComposeEffectsDemo).

Also, if we've missed your favorite effects library please excuse me. We're happy, if enough interest, to work with you incorporate adapters to provide further demonstration of integrating the beloved effect library. Focus of OfficeFloor is not to be opinionated but rather provide an open platform to integrate software.

The next article in the series tests my self taught mathematics to attempt to explain the underlying model of why this ease of composition is possible.

Further Reading

Reactive Programming With Project Reactor

The False Hope of Managing Effects With Tagless-Final in Scala (Part 1)

Topics:
cats effect ,reactor ,zio ,effect ,composition ,java ,tutorial

Published at DZone with permission of Daniel Sagenschneider . See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}