Style Guide
Effect style guide is a set of recommendations for writing Effect modules.
Modules
Effect ecosystem consists of many modules. Always use import * as ...
to avoid name conflicts. Modules are like namespaces.
ts
import * as Effect from "@effect/io/Effect"import * as Either from "@effect/data/Either"import * as Schema from "@effect/schema/Schema"// Effect.runCallback// Either.fromNullable// Schema.struct
ts
import * as Effect from "@effect/io/Effect"import * as Either from "@effect/data/Either"import * as Schema from "@effect/schema/Schema"// Effect.runCallback// Either.fromNullable// Schema.struct
Types and interfaces should be "namespaced" as well. You may think it's verbose, but it's explicit, and the code written in this style will never clash.
Effect.Effect<SomeRequirement, SomeError, SomeResult>
Effect.Effect<SomeRequirement, SomeError, SomeResult>
There is only one exception. Functions from module Function shall be used directly.
ts
import { pipe } from "@effect/data/Function"
ts
import { pipe } from "@effect/data/Function"
Writing Code with Effect
Nested callbacks are a common problem in JavaScript, and are often handled by using async/await in most modern JavaScript codebases. However, since we are working in the world of Effects, the available APIs are a bit different.
This section will cover the three common ways to organize Effectful code:
-
Using
pipe
-
Using generators
-
Using the
Do
simulation
Using pipe
The pipe function is a common way to organize Effectful code. It is a function that takes a value and a series of functions, and applies the functions to the value in order.
The pipe function is defined in the Effect library, and is available in the Effect namespace.
ts
import {pipe } from "@effect/data/Function"import * asEffect from "@effect/io/Effect"import * asRandom from "@effect/io/Random"constexample =pipe (Random .next (),Effect .flatMap ((n ) =>n > 0.5 ?Effect .succeed ("yay!") :Effect .fail ("oh no!")))
ts
import {pipe } from "@effect/data/Function"import * asEffect from "@effect/io/Effect"import * asRandom from "@effect/io/Random"constexample =pipe (Random .next (),Effect .flatMap ((n ) =>n > 0.5 ?Effect .succeed ("yay!") :Effect .fail ("oh no!")))
Here, Random.next()
is getting piped as an argument to Effect.flatMap
.
Many functions in Effect have dual
APIs, which means that you can either
call it data-first
or data-last
. Here, you can see how Effect.flatMap
is
after the call to Random.next()
in the pipe
, so Random.next()
is
passed in last (i.e. data-last
). Similarly, you can also call
Effect.flatMap(Random.next(), (n) => ...)
directly without pipe
with
Random.next()
as the first argument (i.e. data-first
) and get the same
result.
Using generators
Effect has a unique imperative API to solve for the problem of lots of nested callbacks when using functions like pipe
. Instead of writing the code above, here's what it would look like if you were to write the same thing with the generator API.
ts
import * asEffect from "@effect/io/Effect"import * asRandom from "@effect/io/Random"constexample =Effect .gen (function* ($ ) {constn = yield*$ (Random .next ())if (n > 0.5) {return yield*$ (Effect .succeed ("yay!"))} else {return yield*$ (Effect .fail ("oh no!"))}})
ts
import * asEffect from "@effect/io/Effect"import * asRandom from "@effect/io/Random"constexample =Effect .gen (function* ($ ) {constn = yield*$ (Random .next ())if (n > 0.5) {return yield*$ (Effect .succeed ("yay!"))} else {return yield*$ (Effect .fail ("oh no!"))}})
Note the yield*
, which is used to yield the next value from the generator. Additionally, one can also observe the $
argument passed to Effect.gen
, which is just a function that serves as an "adapter" that allows TypeScript to track our Effect types. When yielding an Effect, you must pass the Effect you would like to yield*
to the adapter function to ensure that TypeScript can properly infer the types.
This way, you can write in a more imperative style rather than having to use pipe
and flatMap
.
The dollar sign is just an argument name convention and is not a special
symbol in Effect. You are free to name it whatever you want (e.g. using an
underscore, etc.). The current convention is to use $
as the argument name.
Using the Adapter Argument as a pipe
One thing to note is that the $
argument also can be used as a pipe function. In the following example, Random.next()
is piped into Effect.either
.
ts
import * asEffect from "@effect/io/Effect"import * asRandom from "@effect/io/Random"import * asEither from "@effect/data/Either"constexample =Effect .gen (function* ($ ) {constn = yield*$ (Random .next (),Effect .either )if (Either .isRight (n )) {return yield*$ (Effect .succeed ("yay!"))} else {return yield*$ (Effect .fail ("oh no!"))}})
ts
import * asEffect from "@effect/io/Effect"import * asRandom from "@effect/io/Random"import * asEither from "@effect/data/Either"constexample =Effect .gen (function* ($ ) {constn = yield*$ (Random .next (),Effect .either )if (Either .isRight (n )) {return yield*$ (Effect .succeed ("yay!"))} else {return yield*$ (Effect .fail ("oh no!"))}})
Using the Do
simulation
To avoid nested pipes and generators, Effect has a Do
function which can be used to simulate Do
notation in other languages.
If you are familiar with Haskell or Scala, you may be familiar with Do
notation or Scala's for
comprehensions. Effect has a similar API.
Here's what a slightly more complex version of the above example would look like if you were to use Do
.
ts
import * asEffect from "@effect/io/Effect"import * asRandom from "@effect/io/Random"import {pipe } from "@effect/data/Function"constexample =pipe (Effect .Do (),Effect .let ("randomValue", () => 3),Effect .bind ("randomNumber", ({randomValue }) =>Random .next ()),Effect .flatMap (({randomNumber ,randomValue }) =>randomNumber > 0.5 &&randomValue > 1?Effect .succeed ("yay!"):Effect .fail ("oh no!")))
ts
import * asEffect from "@effect/io/Effect"import * asRandom from "@effect/io/Random"import {pipe } from "@effect/data/Function"constexample =pipe (Effect .Do (),Effect .let ("randomValue", () => 3),Effect .bind ("randomNumber", ({randomValue }) =>Random .next ()),Effect .flatMap (({randomNumber ,randomValue }) =>randomNumber > 0.5 &&randomValue > 1?Effect .succeed ("yay!"):Effect .fail ("oh no!")))
Using Effect.let
is similar to saving a variable to a value, while
Effect.bind
will unwrap an Effect similar to Effect.flatMap
.
The strings passed to bind
become keys in an object, and the values being the result on the right. When using these functions, notice how each successive function is passed the previous result, similar to if you had set a variable in the generator example above.
Errors
Define errors with Error
suffix.
TODO: