The Effect Data Types: Effect

A deep dive into the specific of the @effect-ts/core/Effect segment. Continue reading to learn more!

The Effect Data Types: Effect

Matechs Effect Core (Series)

1. Encoding HKTs in TS4.1

2. Effect-TS Core: ZIO-Prelude Inspired Typeclasses & Module Structure

3. The Effect Data Types: Effect

4. The Effect Data Types: Managed & Layer

5. Abusing TypeScript Generators

In the first 2 parts of the series we have looked at the overall module structure of @effect-ts/core and discussed the general aspects of how modules are organised.

We will now take a deep dive into the specific of the @effect-ts/core/Effect segment, the primary target for the modules that we will discuss today is node.js and in general backend development.

While the technologies in question are perfectly usable in the frontend space we believe most of the features are overkill if used on top of pre-existing frameworks like react and that within the @effect-ts/core system there are more appropriate data types like Async & Sync and in general the ones available in the @effect-ts/core/Classic segment that better fit the requirements.

In general the advise will be to use Effect and its derivative in frontend development only if major usage is involved and the bundle-size vs feature-set make sense for you (Effect and its derivatives tend to add a minimum of 30-80k), for example we have seen very successful integrations of Stream with a redux epic-inspired design but we have also seen adding 50k to the bundle to simply have interruption on a Promise (that would be something to avoid).

What we will discuss today (and in the next few articles of the series) in most parts is directly usable across all the data types that implements instances of Environmental thanks to the approach discussed in the previous posts.

The Effect Type

Let's start with the basic and let's take a look at the type definition.

coding screenshot

the real definition of Effect is omitted from the purpose of this discussion because the reality is you have no reasons in the world to ever look inside of it.

The logical definition that we can think of is in the lines of:

coding screenshot

Basically the Effect type describes a computation that, in order to run requires an argument of type R to be provided and when the computation runs and completes will produce a result that can either be a failure of type E or a success of type A.

In reality the implementation uses a different internal representation and makes usage of fibers to represent concurrent computations and additionally has an unchecked exception channel to signal defects of code or unexpected exceptions, the Effect type uses Cause to represent the error case of the Exit and Cause allows for tracking multiple exit causes with different methods (like parallel/sequential cause of failures, interruptions, etc).

Let's take a look at a basic usage of all the parameters:

coding screenshot

If we highlight on the type of division:

coding screenshot

We can easily read that in order to run this computation we will be required to provide an argument of type Input and we can expect either an error of type string or a success of type void (nothing basically).

In the last line of the snippet we proceed to run the program with:

coding screenshot

If we remove T.provideAll({ x: 1, y: 2 }) we will be alerted with a compiler error that state Input is not assignable to DefaultEnv that translates to: "you need to give Input otherwise I have no idea what to run".

If we run the computation with y = 0 we will get:

coding screenshot

Using runMain we have automatic reporting of errors in a prettified format (we will take a look at a larger report later), additionally runMain returns a function that when called will cancel the running computation.

Snippets Location

All the snippets in this article are available at: https://github.com/Matechs-Garage/effect-ts-bootstrap/tree/master/articles/effect

In the repository you find also a playground environment that bootstrap a postgresql, runs migrations and implements a basic persistence module with detailed integration tests some of which using arbitrary generated models (property-based), we will cover those aspects in future articles.

Variance

Like we discussed in the previous articles of the series variance is a key component in the design of the modules of @effect-ts/core in the case of Effect we discussed 3 type parameters R, E, A representing one input R one possible error (output) E and one possible output A.

In mostly all of the available combinators E is treated as co-variant and R is treated as contra-variant, this means that, by default, mixing different Effects the result will be an Effect that requires R1 & R2 and can fail for E1 | E2.

Let's see it in practice:

coding screenshot

Handling Checked Errors

In the previous example we used a tagged union to represent the error type, that choice was purposely made because of typescript's ability to automatically discriminate over the members of such union.

Let's see for example how we could handle the ErrorA case of the program above:

coding screenshot

If you use this standard for errors and tagged unions in general you can leverage the pattern & matchTag utilities of @effect-ts/core/Utils and rewrite the above as:

coding screenshot

Unchecked Exceptions

So far we've seen how to raise checked errors that have specific types and are safely represented for the type-checker to process, there are cases where instead we want to raise some stronger form of error, for example to handle cases like a code defect or cases where unknown system errors have been observed, to do so Effect provides a dedicated Error channel.

Note that raising on this channel will skip any checked error management process like the above T.catchAll.

Let's make an example:

coding screenshot

By running the code above we will get:

coding screenshot

We can notice none of the error handling logic have been accounted for, in fact neither console.log cases of the T.catchAll logic have been produced.


Handling full Cause

In order to take control over the full exit cause of a program it is necessary to access lower level combinators like foldCause or a combination of sandbox/catchAll.

Let's take a look at a possibility:

coding screenshot

With the combinator Cause.defects we can extract a list of defects that might be present on the cause, there are many more combinators you can use to encode the many possible scenarios you might need to handle.

The type definition Cause is as follows:

coding screenshot

Let's take a look at a more complex code where we might trigger multiple failures and let's take a look at how it gets reported:

coding screenshot

Running this various times will produce different outputs given the random nature of the program but an average result will look like:

coding screenshot

We can see that at the first failure the tuplePar combinator takes care of interrupting the other running computations and we can see by the horizontal nature of the report that there were running 6 fibers in total.

Let's take a look at how error handling applies in this scenarios:

coding screenshot

This will now emit something like:

coding screenshot

We can see that catchAll peaks at the first available checked error in the cause tree and handles that.

In order to handle multiple causes of failures we can, as outlined before, jump in a sandbox and traverse the cause with available combinators like collect and similar.

Environment

We have covered up to now the usage of E and A, let's now take a deep look at how R works.

The crucial idea we will discuss here is I believe the single 10x improvement ZIO had to offer to the scala ecosystem and it applies perfectly (even more compared to scala) to the structural variance of typescript.

The main idea is to embed dynamic services that contains business logic as part of the environment having as a consequence a type-level representation of what modules a specific computation needs in order to run, for example in previous snippets we have interacted directly with console.log, that code will easily become non testable without weird tricks like monkey-patching modules.

Let's start with the basic:

coding screenshot

When executed this program will output:

coding screenshot

We can now provide a different implementation that we might use to test the correct implementation of program without output to console, for example:

coding screenshot

In this way we can run the program without ever interacting with the actual console.

Has & Tag

It's all nice but at some point we will encounter strange situations, the intersection of 2 types is not generally commutative and can lead to very strange behaviours.

One example would be having 2 services implement the log function with different signatures, those will be collapsed to never when ServiceA & ServiceB are present, additionally inference cannot always discern those members from the mangled result, we need to remember, at the end, that the typescript type-system is structural.

Here comes Has and Tag to solve the issue, Tag<t> represent an object that has the ability to read and write from and to and environment of type Has<t>, Has is designed to be safe for intersection.</t></t>

Let's rewrite the above using Has and Tag:

coding screenshot

There are many combinators to work with services (provide, access, replace, etc).

Services are also highly used in structuring modular environments constructed dynamically using Layer but that discussion is for another post of the series, after having seen the Managed data type first.

We can already improve the code a little by deriving the implementation of log, to do so we can look at deriveLifted:

coding screenshot

The 3 array parameters of deriveLifted represent the field names of the functions to derive and they represent in sequence:

  1. non generic, effect-returning functions
  2. constant effects
  3. constant values

Usage:

coding screenshot


Additional combinators like deriveAccess and deriveAccessM can be used to derive access-like function signatures like:

coding screenshot

Effect Core

1. Encoding HKTs in TS4.1

2. Effect-TS Core: ZIO-Prelude Inspired Typeclasses & Module Structure

3. The Effect Data Types: Effect

4. The Effect Data Types: Managed & Layer

5. Abusing TypeScript Generators

Stay updated on our current and upcoming training workshops!

Be social
Be social