Documentation
Control Flow

Introduction to Effect's Control Flow Operators

Even though JavaScript provides built-in control flow structures, Effect offers additional control flow functions that are useful in Effect applications. In this section, we will introduce different ways to control the flow of execution.

if Expression

When working with Effect values, we can use the standard JavaScript if-then-else expressions:

import { Effect, Option } from "effect"
 
const validateWeightOption = (
  weight: number
): Effect.Effect<never, never, Option.Option<number>> => {
  if (weight >= 0) {
    return Effect.succeed(Option.some(weight))
  } else {
    return Effect.succeed(Option.none())
  }
}

Here we are using the Option data type to represent the absence of a valid value.

We can also handle invalid inputs by using the error channel:

import { Effect } from "effect"
 
const validateWeightOrFail = (
  weight: number
): Effect.Effect<never, string, number> => {
  if (weight >= 0) {
    return Effect.succeed(weight)
  } else {
    return Effect.fail(`negative input: ${weight}`)
  }
}

Conditional Operators

when

Instead of using if (condition) expression, we can use the Effect.when function:

import { Effect, Option } from "effect"
 
const validateWeightOption = (
  weight: number
): Effect.Effect<never, never, Option.Option<number>> =>
  Effect.succeed(weight).pipe(Effect.when(() => weight >= 0))

Here we are using the Option data type to represent the absence of a valid value.

If the condition evaluates to true, the effect inside the Effect.when will be executed and the result will be wrapped in a Some, otherwise it returns None:

Effect.runPromise(validateWeightOption(100)).then(console.log)
/*
Output:
{
  _id: "Option",
  _tag: "Some",
  value: 100
}
*/
 
Effect.runPromise(validateWeightOption(-5)).then(console.log)
/*
Output:
{
  _id: "Option",
  _tag: "None"
}
*/

If the condition itself involves an effect, we can use Effect.whenEffect.

For example, the following function creates a random option of an integer value:

import { Effect, Random } from "effect"
 
// $ExpectType Effect<never, never, Option<number>>
const randomIntOption = Random.nextInt.pipe(
  Effect.whenEffect(Random.nextBoolean)
)

unless

The Effect.unless and Effect.unlessEffect functions are similar to the when* functions, but they are equivalent to the if (!condition) expression construct.

if

The Effect.if function allows you to provide an effectful predicate. If the predicate evaluates to true, the onTrue effect will be executed. Otherwise, the onFalse effect will be executed.

Let's use this function to create a simple virtual coin flip function:

import { Effect, Random, Console } from "effect"
 
// $ExpectType Effect<never, never, void>
const flipTheCoin = Effect.if(Random.nextBoolean, {
  onTrue: Console.log("Head"),
  onFalse: Console.log("Tail")
})
 
Effect.runPromise(flipTheCoin)

In this example, we generate a random boolean value using Random.nextBoolean. If the value is true, the effect onTrue will be executed, which logs "Head". Otherwise, if the value is false, the effect onFalse will be executed, logging "Tail".

Loop Operators

loop

The Effect.loop function allows you to repeatedly change the state based on an step function until a condition given by the while function is evaluated to true:

Effect.loop(initial, options: { while, step, body })

It collects all intermediate states in an array and returns it as the final result.

We can think of Effect.loop as equivalent to a while loop in JavaScript:

let state = initial
const result = []
 
while (options.while(state)) {
  result.push(options.body(state))
  state = options.step(state)
}
 
return result

Example

import { Effect } from "effect"
 
// $ExpectType Effect<never, never, number[]>
const result = Effect.loop(
  1, // Initial state
  {
    while: (state) => state <= 5, // Condition to continue looping
    step: (state) => state + 1, // State update function
    body: (state) => Effect.succeed(state) // Effect to be performed on each iteration
  }
)
 
Effect.runPromise(result).then(console.log) // Output: [1, 2, 3, 4, 5]

In this example, the loop starts with an initial state of 1. The loop continues as long as the condition n <= 5 is true, and in each iteration, the state n is incremented by 1. The effect Effect.succeed(n) is performed on each iteration, collecting all intermediate states in an array.

You can also use the discard option if you're not interested in collecting the intermediate results. It discards all intermediate states and returns undefined as the final result.

Example (discard: true)

import { Effect, Console } from "effect"
 
// $ExpectType Effect<never, never, void>
const result = Effect.loop(
  1, // Initial state
  {
    while: (state) => state <= 5, // Condition to continue looping,
    step: (state) => state + 1, // State update function,
    body: (state) => Console.log(`Currently at state ${state}`), // Effect to be performed on each iteration,
    discard: true
  }
)
 
Effect.runPromise(result).then(console.log)
/*
Output:
Currently at state 1
Currently at state 2
Currently at state 3
Currently at state 4
Currently at state 5
undefined
*/

In this example, the loop performs a side effect of logging the current index on each iteration, but it discards all intermediate results. The final result is undefined.

iterate

The Effect.iterate function allows you to iterate with an effectful operation. It uses an effectful body operation to change the state during each iteration and continues the iteration as long as the while function evaluates to true:

Effect.iterate(initial, options: { while, body })

We can think of Effect.iterate as equivalent to a while loop in JavaScript:

let result = initial
 
while (options.while(result)) {
  result = options.body(result)
}
 
return result

Here's an example of how it works:

import { Effect } from "effect"
 
// $ExpectType Effect<never, never, number>
const result = Effect.iterate(
  1, // Initial result
  {
    while: (result) => result <= 5, // Condition to continue iterating
    body: (result) => Effect.succeed(result + 1) // Operation to change the result
  }
)
 
Effect.runPromise(result).then(console.log) // Output: 6

forEach

The Effect.forEach function allows you to iterate over an Iterable and perform an effectful operation for each element.

The syntax for forEach is as follows:

import { Effect } from "effect"
 
const combinedEffect = Effect.forEach(iterable, operation, options)

It applies the given effectful operation to each element of the Iterable. By default, it executes each effect in sequence (to explore options for managing concurrency and controlling how these effects are executed, you can refer to the Concurrency Options documentation).

This function returns a new effect that produces an array containing the results of each individual effect.

Let's take a look at an example:

import { Effect, Console } from "effect"
 
// $ExpectType Effect<never, never, number[]>
const result = Effect.forEach([1, 2, 3, 4, 5], (n, index) =>
  Console.log(`Currently at index ${index}`).pipe(Effect.as(n * 2))
)
 
Effect.runPromise(result).then(console.log)
/*
Output:
Currently at index 0
Currently at index 1
Currently at index 2
Currently at index 3
Currently at index 4
[ 2, 4, 6, 8, 10 ]
*/

In this example, we have an array [1, 2, 3, 4, 5], and for each element we perform an effectful operation. The output shows that the operation is executed for each element in the array, displaying the current index.

The Effect.forEach combinator collects the results of each effectful operation in an array, which is why the final output is [ 2, 4, 6, 8, 10 ].

We also have the discard option, which when set to true discards the results of each effectful operation:

import { Effect, Console } from "effect"
 
// $ExpectType Effect<never, never, void>
const result = Effect.forEach(
  [1, 2, 3, 4, 5],
  (n, index) =>
    Console.log(`Currently at index ${index}`).pipe(Effect.as(n * 2)),
  { discard: true }
)
 
Effect.runPromise(result).then(console.log)
/*
Output:
Currently at index 0
Currently at index 1
Currently at index 2
Currently at index 3
Currently at index 4
undefined
*/

In this case, the output is the same, but the final result is undefined since the results of each effectful operation are discarded.

all

The Effect.all function in the Effect library is a powerful tool that allows you to merge multiple effects into a single effect, offering flexibility by working with various structured formats such as tuples, iterables, structs, and records.

The syntax for all is as follows:

import { Effect } from "effect"
 
const combinedEffect = Effect.all(effects, options)

where effects is a collection of individual effects that you wish to merge.

By default, the all function will execute each effect in sequence (to explore options for managing concurrency and controlling how these effects are executed, you can refer to the Concurrency Options documentation).

It will return a new effect that produces a result with a shape that depends on the shape of the effects argument.

Let's explore examples for each supported shape: tuples, iterables, structs, and records.

Tuples

import { Effect, Console } from "effect"
 
const tuple = [
  Effect.succeed(42).pipe(Effect.tap(Console.log)),
  Effect.succeed("Hello").pipe(Effect.tap(Console.log))
] as const
 
// $ExpectType Effect<never, never, [number, string]>
const combinedEffect = Effect.all(tuple)
 
Effect.runPromise(combinedEffect).then(console.log)
/*
Output:
42
Hello
[ 42, 'Hello' ]
*/

Iterables

import { Effect, Console } from "effect"
 
const iterable: Iterable<Effect.Effect<never, never, number>> = [1, 2, 3].map(
  (n) => Effect.succeed(n).pipe(Effect.tap(Console.log))
)
 
// $ExpectType Effect<never, never, number[]>
const combinedEffect = Effect.all(iterable)
 
Effect.runPromise(combinedEffect).then(console.log)
/*
Output:
1
2
3
[ 1, 2, 3 ]
*/

Structs

import { Effect, Console } from "effect"
 
const struct = {
  a: Effect.succeed(42).pipe(Effect.tap(Console.log)),
  b: Effect.succeed("Hello").pipe(Effect.tap(Console.log))
}
 
// $ExpectType Effect<never, never, { a: number; b: string; }>
const combinedEffect = Effect.all(struct)
 
Effect.runPromise(combinedEffect).then(console.log)
/*
Output:
42
Hello
{ a: 42, b: 'Hello' }
*/

Records

import { Effect, Console } from "effect"
 
const record: Record<string, Effect.Effect<never, never, number>> = {
  key1: Effect.succeed(1).pipe(Effect.tap(Console.log)),
  key2: Effect.succeed(2).pipe(Effect.tap(Console.log))
}
 
// $ExpectType Effect<never, never, { [x: string]: number; }>
const combinedEffect = Effect.all(record)
 
Effect.runPromise(combinedEffect).then(console.log)
/*
Output:
1
2
{ key1: 1, key2: 2 }
*/