Docs
Style Guide

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:

  1. Using pipe

  2. Using generators

  3. 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 * as Effect from "@effect/io/Effect"
import * as Random from "@effect/io/Random"
 
const example = pipe(
Random.next(),
Effect.flatMap((n) =>
n > 0.5 ? Effect.succeed("yay!") : Effect.fail("oh no!")
)
)
ts
import { pipe } from "@effect/data/Function"
import * as Effect from "@effect/io/Effect"
import * as Random from "@effect/io/Random"
 
const example = 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 * as Effect from "@effect/io/Effect"
import * as Random from "@effect/io/Random"
 
const example = Effect.gen(function* ($) {
const n = yield* $(Random.next())
if (n > 0.5) {
return yield* $(Effect.succeed("yay!"))
} else {
return yield* $(Effect.fail("oh no!"))
}
})
ts
import * as Effect from "@effect/io/Effect"
import * as Random from "@effect/io/Random"
 
const example = Effect.gen(function* ($) {
const n = 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 * as Effect from "@effect/io/Effect"
import * as Random from "@effect/io/Random"
import * as Either from "@effect/data/Either"
 
const example = Effect.gen(function* ($) {
const n = yield* $(Random.next(), Effect.either)
if (Either.isRight(n)) {
return yield* $(Effect.succeed("yay!"))
} else {
return yield* $(Effect.fail("oh no!"))
}
})
ts
import * as Effect from "@effect/io/Effect"
import * as Random from "@effect/io/Random"
import * as Either from "@effect/data/Either"
 
const example = Effect.gen(function* ($) {
const n = 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 * as Effect from "@effect/io/Effect"
import * as Random from "@effect/io/Random"
import { pipe } from "@effect/data/Function"
 
const example = 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 * as Effect from "@effect/io/Effect"
import * as Random from "@effect/io/Random"
import { pipe } from "@effect/data/Function"
 
const example = 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: