In the previous post of the series we started to explore the Effect data type in details.
The Effect type is itself used to construct a variety of data-types that are thought to cover many of the common patterns of software development.
This time we will explore Managed and Layer both constructed using Effect, the first uses to safely allocate and release resources and the second used to construct environments.
In the code snippets we will simulate data stores by making usage of a third data-type built on Effect the Ref data-type, Ref<t> encodes a mutable Reference to an immutable value T and can be used to keep track of State through a computation.</t>
Let's start by taking a look at a classic pattern we encounter many times: bracket
Bracket is used to safely acquire Resources like DbConnection to be used and safely released post usage.
The idea behind Managed is to generalize the behaviour of bracket by isolating the declaration of acquire and release into a specific data-type.
The inner details of how this data-type works are out of the scope of this post but the design is a very interesting (more advanced) topic to explore. It is based on the implementation of ZManaged in ZIO that is itself based on the ResourceT designed by Michael Snoyman in haskell.
Let's take a look at the same code when isolated into a Managed:
We can see how we moved the release step from our program into the data-type of managedDb, now we can use the Resource without having to track its closing state.
One other advantage that we gained is we can now easily compose multiple Resources together, let's take a look at how we might do that:
We have added a second Managed called managedBroker to simulate a connection to a message broker and we composed the 2 using zip.
The result will be a Managed that acquires both Resources and release both Resources as you would expect.
There are many combinators in the Managed module, for example a simple change from zip to zipPar would change the behaviour of the composed Managed and will acquire and release both Resources in parallel.
The Layer data-type is closely related to Managed, in fact the idea for the design of Layer has its roots in a pattern commonly discovered in the early days of ZIO by early adopters.
We have looked at the Effect data-type already and we notice how the environment R can be used to embed services in order to gain great testability and code organization.
The question now is, how do I construct those environments?
It seems simple in principle but when you get to write an app this embedding of services becomes so pervasive that is completely normal to find yourself embedding hundreds of services in your "main" program.
Many of the services are going to be dependent on database connections and stuff like that, and as we saw before Managed is a good use case for those.
The community has started to construct their environments by using Managed so that services are constructed at bootstrap time and the result is used to provide the environment.
This pattern progressively evolved to an independent data-type that is based on Managed tailored to specifically to construct environments.
Let's start by simply transforming our code to use Layers, it will be straightforward given we have already designed it using Managed:
What we did is we turned our Managed into Layers by using the fromRawManaged constructor and we then plugged the results into a single ProgramLive layer.
We then transformed our program to consume the DbConnection and BrokerConnection services from environment.
At the very end we provided the ProgramLive to our program by using provideSomeLayer.
The behaviour itself remained the same but we have now fully separated program definition from resources and services location.
In the last article of the series we scratched the surface of the Has & Tag apis, we are going to see now a bit more details about the inner working and how it integrates into Layers.
Up to now we haven't noticed issues because we took care of keeping function names like send and get separated but in reality it is not a good idea to intersect casual services into a big object because conflicts might easily arise.
One conflict we had without even noticing (because it's used only locally inside managed) is the "clear" effect of both DbConnection and BrokerConnection, that will get shadowed at runtime in the intersected result.
In order to solve this issue, and in order to improve the amount of types you end up writing in your app we designed two data-types that solve the issue.
We took inspiration from the work done in ZIO where the Has type is explicit and the Tag type is implicit in typescript we had to come up with an explicit solution.
The trick works like that:
Fundamentally Tag<t> encodes the capability of reading T from a type Has<t> and encodes the capability of producing a value of type Has<t> given a T.</t></t></t>
With the guarantee that given T0 and T1 the result Has<t0> & Has<t1> is always discriminated.</t1></t0>
Let's see it practice by rewriting our code:
By this very simple change we are now safe, and as we can already see, instead of manually annotate the type DbConnection to access the service we can now simply use the Tag value.
We can go a bit further and derive some functions that we would normally like to write, the idea is:
You don't want that in code.
To avoid repeating this you end-up writing commodity functions like:
and use this instead of direct access.
We can automatically derive such implementations in many of the common function types except the case of functions containing generics.
Let's see it in practice:
The other 2 arrays of deriveLifted are used to generate functions to access Constant Effects and Constants as Effects.
Like deriveLifted other 2 functions can be used to derive commonly used utilities and those are deriveAccess and deriveAccessM.
It is left as an exercise to the reader to try out those combinations and look at the result function types in order to better understand the context of each derivation.
Another way to smooth the usage of services without the need to actually write any of the utility functions is to effectively write your main program as a service and leverage the layer constructor utilities to automate the environment wiring.
Let's see how we might do that:
We basically introduced a new service Program as a constructor makeProgram that simply takes dependencies as arguments and we used the Layer constructor fromConstructor to glue everything.
We had to add the ProgramLive layer to our final MainLive cake and in the main function we called the effect from the program service (that can be easily derived as before).