Using Generators in Effect

On this page

In the previous sections, we learned how to create effects and execute them. Now, it's time to write our first simple program.

Effect offers a convenient syntax, similar to async/await, to write effectful code using generators.

The use of generators is an optional feature in Effect. If you find generators unfamiliar or prefer a different coding style, you can explore the documentation about Building Pipelines in Effect.

Understanding Effect.gen

Let's start with a basic program that performs a series of transformations:

ts
import { Effect } from "effect"
 
const increment = (x: number) => x + 1
 
const divide = (a: number, b: number): Effect.Effect<number, Error> =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
 
const task1 = Effect.promise(() => Promise.resolve(10))
 
const task2 = Effect.promise(() => Promise.resolve(2))
 
export const program = Effect.gen(function* (_) {
const a = yield* _(task1)
const b = yield* _(task2)
const n1 = yield* _(divide(a, b))
const n2 = increment(n1)
return `Result is: ${n2}`
})
 
Effect.runPromise(program).then(console.log) // Output: "Result is: 6"
ts
import { Effect } from "effect"
 
const increment = (x: number) => x + 1
 
const divide = (a: number, b: number): Effect.Effect<number, Error> =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
 
const task1 = Effect.promise(() => Promise.resolve(10))
 
const task2 = Effect.promise(() => Promise.resolve(2))
 
export const program = Effect.gen(function* (_) {
const a = yield* _(task1)
const b = yield* _(task2)
const n1 = yield* _(divide(a, b))
const n2 = increment(n1)
return `Result is: ${n2}`
})
 
Effect.runPromise(program).then(console.log) // Output: "Result is: 6"

The generator API is only available when using the downlevelIteration flag or with a target of "es2015" or higher in your tsconfig.json file

When working with generators in Effect, the _ helper plays a crucial role when yielding an effect. By passing the effect you want to yield to the _ function...

ts
export const program = Effect.gen(function* (_) {
const a = yield* _(task1)
const b = yield* _(task2)
const n1 = yield* _(divide(a, b))
const n2 = increment(n1)
return `Result is: ${n2}`
})
ts
export const program = Effect.gen(function* (_) {
const a = yield* _(task1)
const b = yield* _(task2)
const n1 = yield* _(divide(a, b))
const n2 = increment(n1)
return `Result is: ${n2}`
})

...TypeScript can accurately infer the types associated with that effect. This ensures that your code is type-safe and helps prevent potential errors.

Additionally, the _ function acts as an adapter between Effect and the JavaScript world, particularly the iterable protocol. This adapter allows you to seamlessly leverage the yield* keyword from JavaScript's generator syntax within Effect's generator functions.

The _ symbol is just a convention for the argument name and is not a special symbol in Effect. You are free to use any name you prefer (e.g., $, etc...). The current convention is to use _ as the argument name.

Comparing Effect.gen with async/await

If you are familiar with async/await, you may notice that the flow of writing code is similar.

Let's compare the two approaches:


ts
const increment = (x: number) => x + 1
 
const divide = (a: number, b: number): Effect.Effect<number, Error> =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
 
const task1 = Effect.promise(() => Promise.resolve(10))
 
const task2 = Effect.promise(() => Promise.resolve(2))
 
export const program = Effect.gen(function* (_) {
const a = yield* _(task1)
const b = yield* _(task2)
const n1 = yield* _(divide(a, b))
const n2 = increment(n1)
return `Result is: ${n2}`
})
 
Effect.runPromise(program).then(console.log) // Output: "Result is: 6"
ts
const increment = (x: number) => x + 1
 
const divide = (a: number, b: number): Effect.Effect<number, Error> =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
 
const task1 = Effect.promise(() => Promise.resolve(10))
 
const task2 = Effect.promise(() => Promise.resolve(2))
 
export const program = Effect.gen(function* (_) {
const a = yield* _(task1)
const b = yield* _(task2)
const n1 = yield* _(divide(a, b))
const n2 = increment(n1)
return `Result is: ${n2}`
})
 
Effect.runPromise(program).then(console.log) // Output: "Result is: 6"

It's important to note that although the code appears similar, the two programs are not identical. The purpose of comparing them side by side is just to highlight the resemblance in how they are written.

Embracing Control Flow

One significant advantage of using Effect.gen in conjunction with generators is its capability to employ standard control flow constructs within the generator function. These constructs encompass if/else, for, while, and other branching and looping mechanisms, enhancing your ability to express complex control flow logic in your code.

To illustrate this, let's delve into a practical example:

ts
import { Effect } from "effect"
 
const divide = (a: number, b: number): Effect.Effect<number, Error> =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
 
const program = Effect.gen(function* (_) {
let i = 1
 
while (true) {
if (i === 10) {
break
} else {
if (i % 2 === 0) {
console.log(yield* _(divide(12, i)))
}
i++
continue
}
}
})
 
Effect.runPromise(program)
/*
Output:
6
3
2
1.5
*/
ts
import { Effect } from "effect"
 
const divide = (a: number, b: number): Effect.Effect<number, Error> =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
 
const program = Effect.gen(function* (_) {
let i = 1
 
while (true) {
if (i === 10) {
break
} else {
if (i % 2 === 0) {
console.log(yield* _(divide(12, i)))
}
i++
continue
}
}
})
 
Effect.runPromise(program)
/*
Output:
6
3
2
1.5
*/

Raising Errors

Within the realm of the Effect.gen API, you have the capability to introduce errors into your program by using a failed effect as an argument to the _ helper function. This can be effectively achieved, for instance, by using Effect.fail. Let's take a closer look at an example:

ts
import { Effect } from "effect"
 
const program = Effect.gen(function* (_) {
console.log("Task1...")
console.log("Task2...")
yield* _(Effect.fail("Something went wrong!"))
})
 
Effect.runPromise(program).then(console.log, console.error)
/*
Output:
Task1...
Task2...
{
_id: "FiberFailure",
cause: {
_id: "Cause",
_tag: "Fail",
failure: "Something went wrong!"
}
}
*/
ts
import { Effect } from "effect"
 
const program = Effect.gen(function* (_) {
console.log("Task1...")
console.log("Task2...")
yield* _(Effect.fail("Something went wrong!"))
})
 
Effect.runPromise(program).then(console.log, console.error)
/*
Output:
Task1...
Task2...
{
_id: "FiberFailure",
cause: {
_id: "Cause",
_tag: "Fail",
failure: "Something went wrong!"
}
}
*/

In this example, we intentionally introduce an error using Effect.fail to illustrate how errors can be managed within your program.

The Role of Short-Circuiting

When working with the Effect.gen API, it's important to understand how it manages errors. This API is designed to short-circuit the execution upon encountering the first error.

What does this mean for you as a developer? Well, let's say you have a chain of operations or a collection of effects to be executed in sequence. If any error occurs during the execution of one of these effects, the remaining computations will be skipped, and the error will be propagated to the final result.

In simpler terms, the short-circuiting behavior ensures that if something goes wrong at any step of your program it will immediately stop and return the error to let you know that something went wrong.

ts
import { Effect } from "effect"
 
const program = Effect.gen(function* (_) {
console.log("Task1...")
console.log("Task2...")
yield* _(Effect.fail("Something went wrong!"))
console.log("This won't be executed")
})
 
Effect.runPromise(program).then(console.log, console.error)
/*
Output:
Task1...
Task2...
{
_id: "FiberFailure",
cause: {
_id: "Cause",
_tag: "Fail",
failure: "Something went wrong!"
}
}
*/
ts
import { Effect } from "effect"
 
const program = Effect.gen(function* (_) {
console.log("Task1...")
console.log("Task2...")
yield* _(Effect.fail("Something went wrong!"))
console.log("This won't be executed")
})
 
Effect.runPromise(program).then(console.log, console.error)
/*
Output:
Task1...
Task2...
{
_id: "FiberFailure",
cause: {
_id: "Cause",
_tag: "Fail",
failure: "Something went wrong!"
}
}
*/

If you want to dive deeper into effective error handling with Effect, you can explore the "Error Management" section.

Using the helper as a pipe

The _ helper can also be used as a pipe function (see Building Pipelines for more information), allowing you to mix and match different styles of writing code within Effect.gen if needed.

In the following example, the Random.next() effect is piped into the Effect.map function:

ts
import { Effect, Random } from "effect"
 
const program = Effect.gen(function* (_) {
const n = yield* _(
Random.next,
Effect.map((n) => n * 2)
)
if (n > 0.5) {
return "yay!"
} else {
return yield* _(Effect.fail("oh no!"))
}
})
ts
import { Effect, Random } from "effect"
 
const program = Effect.gen(function* (_) {
const n = yield* _(
Random.next,
Effect.map((n) => n * 2)
)
if (n > 0.5) {
return "yay!"
} else {
return yield* _(Effect.fail("oh no!"))
}
})

This approach is useful to avoid excessive notation by using both the _ helper and the pipe function. Instead, you can directly pass the Random.next() effect to Effect.map within the _ helper, eliminating the need for an additional pipe function:

ts
const program = Effect.gen(function* (_) {
const n = yield* _(
pipe(
Random.next,
Effect.map((n) => n * 2)
)
)
// ...
})
ts
const program = Effect.gen(function* (_) {
const n = yield* _(
pipe(
Random.next,
Effect.map((n) => n * 2)
)
)
// ...
})

Passing this

In some cases, you might need to pass a reference to the current object (this) into the body of your generator function. You can achieve this by utilizing an overload that accepts the reference as the first argument:

ts
import { Effect } from "effect"
 
class MyService {
readonly local = 1
compute() {
return Effect.gen(this, function* (_) {
return yield* _(Effect.succeed(this.local + 1))
})
}
}
 
console.log(Effect.runSync(new MyService().compute())) // Output: 2
ts
import { Effect } from "effect"
 
class MyService {
readonly local = 1
compute() {
return Effect.gen(this, function* (_) {
return yield* _(Effect.succeed(this.local + 1))
})
}
}
 
console.log(Effect.runSync(new MyService().compute())) // Output: 2

In this example, we have a MyService class with a property called local. By passing this as the first argument to Effect.gen, we make the local property available within the generator.