Pretty much everything interesting that a computer can do is interacting with the outside world.
Interacting with the outside world is unpredictable, we call programs that interact with the outside world effectful.
Effect is a library that gives predictability to such programs by empowering you with a set of composable building blocks to model the patterns that you'll need on a daily basis.
When writing programs in TypeScript
we are used to code that looks like:
ts
1constgreet = (name : string) => {2constgreeting = `hello ${name }`3console .log (greeting )4returngreeting 5}
ts
1constgreet = (name : string) => {2constgreeting = `hello ${name }`3console .log (greeting )4returngreeting 5}
When such function is invoked like greet("Michael")
the message hello Michael
is printed out in the console.
Whenever any state external to the function scope is mutated (in this case the console state) a function is said to contain side effects.
Another way of looking at the same issue is through the lenses of referential transparency, a pure function (a function that doesn't contain side effects) has the property of respecting evaluation substitution, that means if I have something like f(g(x), g(x))
it is safe to refactor to const y = g(x); f(y, y);
functions with side effects violate this property, for exameple the following two programs yield different outputs:
ts
1constprogram1 = () => {2console .log (`length: ${greet ('Michael') +greet ('Michael')}`)3}45constprogram2 = () => {6constx =greet ('Michael')7console .log (`length: ${x +x }`)8}
ts
1constprogram1 = () => {2console .log (`length: ${greet ('Michael') +greet ('Michael')}`)3}45constprogram2 = () => {6constx =greet ('Michael')7console .log (`length: ${x +x }`)8}
Namely program1
will print twice the message hello Michael
and program2
will print only once.
Code that contains side effects is hard to maintain, it can't be safely refactored and it's fundamentally hard to write in a safe and fast manner.
Turning side effects into effects allows you to deal with such programs in a fully safe manner, let's start by writing the same program with Effect
.
ts
1import * asE from '@effect/core/io/Effect'2import {pipe } from '@tsplus/stdlib/data/Function'34constgreet = (name : string) =>5pipe (6E .succeed (`hello ${name }`),7E .tap ((greeting ) =>E .logInfo (greeting )),8)910constprogram1 =pipe (11greet ('Michael'),12E .zipWith (greet ('Michael'), (x ,y ) =>x +y ),13E .tap ((message ) =>E .logInfo (`length: ${message }`)),14)1516constgreetMichael =greet ('Michael')1718constprogram2 =pipe (19greetMichael ,20E .zipWith (greetMichael , (x ,y ) =>x +y ),21E .flatMap ((message ) =>E .logInfo (`length: ${message }`)),22)
ts
1import * asE from '@effect/core/io/Effect'2import {pipe } from '@tsplus/stdlib/data/Function'34constgreet = (name : string) =>5pipe (6E .succeed (`hello ${name }`),7E .tap ((greeting ) =>E .logInfo (greeting )),8)910constprogram1 =pipe (11greet ('Michael'),12E .zipWith (greet ('Michael'), (x ,y ) =>x +y ),13E .tap ((message ) =>E .logInfo (`length: ${message }`)),14)1516constgreetMichael =greet ('Michael')1718constprogram2 =pipe (19greetMichael ,20E .zipWith (greetMichael , (x ,y ) =>x +y ),21E .flatMap ((message ) =>E .logInfo (`length: ${message }`)),22)
When executed the result of program1
and the one of program2
will be exactly the same, namely twice the message hello Michael
will be printed out in the console followed by a message containing the length of the concatenated strings.
Effect
can execute programs in various ways, the simplest one is the execution to a Promise<Value>
that may be used for interop purposes.
ts
1import * asE from '@effect/core/io/Effect'2import {pipe } from '@tsplus/stdlib/data/Function'34constprogram =pipe (E .logInfo ('Hello'),E .zipRight (E .logInfo ('World')))56E .unsafeRunPromise (program )7.then (() => {8console .log ('Execution Completed')9})10.catch (() => {11console .log ('Execution Failed')12})
ts
1import * asE from '@effect/core/io/Effect'2import {pipe } from '@tsplus/stdlib/data/Function'34constprogram =pipe (E .logInfo ('Hello'),E .zipRight (E .logInfo ('World')))56E .unsafeRunPromise (program )7.then (() => {8console .log ('Execution Completed')9})10.catch (() => {11console .log ('Execution Failed')12})
The Effect
type contains 3 type parameters, the full type looks like Effect<Services, Errors, Success>
and they respectively indicate:
Services
: the list of requirements that your program needsErrors
: the list of errors that your program may encounterSuccess
: the value that your program returnsAs we will see in the next chapters those type parameters are leveraged by Effect
to provide the best possible developer experience when dealing with dependency injection and error handling for your everyday usage.