Decouple the Program from its Implementation with ZIO modules

Pascal Mengelt
5 min readDec 9, 2019

--

Requirements: A basic understanding of ZIO or another ‘Side-Effect Library’.

On the way you will get in contact with mill, yaml, circe-yaml, HOCON and PureConfig - but no worries you do not need to know them.

I am just a user / learner of ZIO and Functional Programming in general.

https://unsplash.com/photos/feeredToXK4

When I first learnt about ZIO / Effect Systems, I read somewhere that with this approach it is possible to separate the Program from its Implementations. Or in other words separate the Domain Logic from the Infrastructure.

Recently I wanted to compare two technologies to:
* read some Configuration Files
* create Case Classes from them
* render the file content from the Case Classes.

After seeing the similarities between the implementations, I wanted to give it a try.

The result can be found here: zio-comps-module and you can read about it here in this Blog ;).

What we want

Evaluate different possibilities to handle Components that are defined by humans.

This diagram shows our Scenario:

The Core

Let’s describe now our Implementation agnostic Core Module.

Domain Model

We have different Components that can be described with configuration files. Each type has its own Component. For example we have DBConnection- or a MessageBundle Component.

There are only dependencies to the standard Scala library.

Service Interface

Our Service contains two functionalities:

  1. You can load a Component from a reference to a Component (CompRef). A CompRef can be resolved to a file on the local file system.
  2. You can render a String of a Component (which then could be for example persisted in a file).

General Program (CompApp)

The general program flow is used by both implementation:

The program loads some Components and renders them. The dbConnection can be retrieved by the dbLookup.dbConRef.

This is a typical flow with ZIO. We describe the happy path in a for-comprehension and in the end we yield 0 (success on the console).

If there are any exception along the way, we want to catchAll, log the problem and return 1 (which indicates an error on the console)

The Service and the Program have only dependencies to ZIO and the standard Scala library.

The Implementations

https://gph.is/2b8Zee8

First stack:

  • YAML as file format
  • circe-yaml / circe for decoding and encoding

Second stack:

  • HOCON as file format
  • PureConfig for decoding / encoding

The HOCON Implementation

So what is needed now to add an actual implementation?

We add a mill Module hocon with its dependency to the core Module and PureConfig (see here the whole build.sc).

loadConfig

As the loadConf function is the complex one, due to its generic nature, I focus on this for now.

PureConfig loads the configuration from the Resource path:

ConfigSource.resources(s”${ref.url}.conf”)

And translates this ConfigSource to the Component via loadOrThrow[T] . This function expects an implicit ConfigReader and ClassTag. We add them as Context Bounds (see FAQ/context-bounds for more infos).

This also needed some help from the community, see Generic Function that returns a specific object.

As this function is synchronous and may throw an Exception, we wrap them with ZIO’s Task.effect. See ZIO documentation for more information.

The return type RIO[Console, T] indicates three things:

  1. T: As the result we yield the created Component.
  2. Console: The environment must be a Console (because we want to log some information to the Console).
  3. RIO: In the Error case, the Error Type will be Throwable. Because RIO[R, T] is a shortcut for ZIO[R, Throwable, T]. See the ZIO type-aliases.

The service implementation

We extend the Components Module, like:

As you can see the signature of the load function is different to the one of the loadConf function.

My first thought was, that as the requiredConfigReader is in scope through import pureconfig.generic.auto._ this should work:

loadConf[T](ref)

But sadly it didn’t:

Error:(37, 18) could not find implicit value for evidence parameter of type pureconfig.ConfigReader[T]
loadConf[T](ref)

So its implementation took me some time to figure out.

First I call loadConf with the Component type, and then map it to the required type T:

loadConf[Component](ref).map { case c: T => c }

This is ugly, as it is more or less a type cast with the according warnings when compiling:

[warn] /../hocon/HoconComps.scala:37:46: abstract type pattern T is unchecked since it is eliminated by erasure
[warn] loadConf[Component](ref).map { case c: T => c }
[warn] ^

Finally adding the ClassTag ContextBound got rid of the warnings:

trait Service[R] {
def load[T <: Component: ClassTag](ref: CompRef): RIO[R, T]
}

Hocon App

Ok - what is now needed to actually use this implementation?

Cool, right? These simple steps are everything:

  1. Extend our general CompApp.
  2. Implement the run function from the zio.App.
  3. Provide our implementation together with the Console to the general Program, that we defined above.

Running the HoconApp prints to the Console.

For a loaded Component:

Component:
DbLookup(postcodeLookup,LocalRef(odsDb),
SELECT f_postcode FROM t_places
WHERE f_name == ?
,Map(name -> Schwändi))

For a rendered Component

Component File postcodeLookup.conf :
{
“db-con-ref” : {
“name” : “odsDb”,
“type” : “local-ref”
},
“name” : “postcodeLookup”,
“params” : {
“name” : “Schwändi”
},
“statement” : “\nSELECT f_postcode FROM t_places\n WHERE f_name == ?\n”,
“type” : “db-lookup”
}

The YAML Implementation

As this is so close, I don’t repeat it here; check the code if you are interested: zio-comps-module/yaml

MyApp

If we want to have just a single App, we create a new Mill module in the build.sc, like

object app extends MyModule {
override def moduleDeps = Seq(yaml, hocon)
}

And add another CompApp:

That’s it. Running them for example with Mill:

# run the YAML implementation:
mill app.run YAML
# run the HOCON implementation:
mill app.run

Conclusion

Photo by Ricardo Gomez Angel on Unsplash

I could solve my main problem — see here for reference : Delegate to a more specific Context Bound.

But other than that, this seems to be a great way to separate the Program from its Implementations.

There are already ideas for the next Blogs 😏, like:

  • How could this be done dynamically, say we define the Implementation in a config-file. Update: done — see How to dynamically inject …
  • How to provide general tests to test any Implementation. Update: done — see How you can use the same Test…
  • Provide a Mock so you can easily use the Module without having any Implementation.

References

The Project to this Blog: zio-comps-module

ZIO: https://zio.dev

Mill: http://www.lihaoyi.com/mill/

PureConfig: https://pureconfig.github.io

circe-yaml: https://github.com/circe/circe-yaml

YAML: https://yaml.org

HOCON: https://github.com/lightbend/config/blob/master/HOCON.md

Thanks to the reviewer Peti Koch.

Let me know if you have questions or stuff that can be done better!

--

--

Pascal Mengelt
Pascal Mengelt

Written by Pascal Mengelt

Working for finnova.com in the Banking business. Prefer to work with Scala / ScalaJS.

No responses yet