Skip to content

Patterns

Looping

In Redux Saga a common pattern for looping is as follows:

function* watcher() {
while (true) {
const action = yield take(ACTION)
yield fork(worker, action.payload)
}
}
function* worker(payload) {
// ... do some stuff
}

In Redux Sonnet, the declarative nature of Effect.forever is more appropriate:

import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
,
import Option

@since2.0.0

@since2.0.0

Option
} from "effect"
import {
import Operators
Operators
} from "redux-sonnet"
declare const
const worker: (action: unknown) => Effect.Effect<void, never, never>
worker
: (
action: unknown
action
: unknown) =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0

@since2.0.0

Effect
<void, never, never>
const
const watcher: Effect.Effect<never, never, SonnetService>
watcher
=
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const gen: <YieldWrap<Effect.Effect<Action, never, SonnetService>> | YieldWrap<Effect.Effect<RuntimeFiber<void, never>, never, never>>, void>(f: (resume: Effect.Adapter) => Generator<...>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

@example

import { Effect } from "effect"
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function* () {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@since2.0.0

gen
(function*(){
const
const action: Action
action
= yield*
import Operators
Operators
.
const unsafeTake: (pred: Predicate<Action>) => Effect.Effect<Action, never, SonnetService>

Take a single Action matching the given predicate.

Warning: This will throw an error if the chunk is empty.

@example

import * as Op from "redux-sonnet/Operators"
const result = Op.unsafeTake(Op.ofType("ACTION"))

@since0.0.0

unsafeTake
(
import Operators
Operators
.
const ofType: <"ACTION">(type: "ACTION") => Refinement<unknown, Action<"ACTION">>

Filter by virtue of Action.type matching the given string.

@example

import { Stream } from "effect"
import { Operators } from "redux-sonnet"
const actions = Stream.make(
{ type: "A1" },
{ type: "A2" },
{ type: "A3" },
)
const filtered = actions.pipe(
Stream.filter(Operators.ofType("A2"))
)
// Effect.runPromise(Stream.runCollect(filtered)).then(console.log)
// { _id: 'Chunk', values: [ { type: "A2"} ] }

@since0.0.0

ofType
('ACTION'))
yield*
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const fork: <void, never, never>(self: Effect.Effect<void, never, never>) => Effect.Effect<RuntimeFiber<void, never>, never, never>

Returns an effect that forks this effect into its own separate fiber, returning the fiber immediately, without waiting for it to begin executing the effect.

You can use the fork method whenever you want to execute an effect in a new fiber, concurrently and without "blocking" the fiber executing other effects. Using fibers can be tricky, so instead of using this method directly, consider other higher-level methods, such as raceWith, zipPar, and so forth.

The fiber returned by this method has methods to interrupt the fiber and to wait for it to finish executing the effect. See Fiber for more information.

Whenever you use this method to launch a new fiber, the new fiber is attached to the parent fiber's scope. This means when the parent fiber terminates, the child fiber will be terminated as well, ensuring that no fibers leak. This behavior is called "auto supervision", and if this behavior is not desired, you may use the forkDaemon or forkIn methods.

@since2.0.0

fork
(
const worker: (action: unknown) => Effect.Effect<void, never, never>
worker
(
const action: Action
action
))
}).
Pipeable.pipe<Effect.Effect<void, never, SonnetService>, Effect.Effect<never, never, SonnetService>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, SonnetService>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const forever: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<never, E, R>

Repeats this effect forever (until the first error).

@since2.0.0

forever
)

Interruption

Redux Saga provides a specialized END action for terminating running “effects.”

import { END } from 'redux-saga'
import { takeEvery } from 'redux-saga/effects'
function* root() {
yield takeEvery("ACTION", worker)
}
function* worker(payload) { /* ... */}
store.dispatch(END)

In Redux Sonnet, we instead may interrupt the forked fiber responsible for managing the root Stanza. There is no concept of a special action.

See packages/redux-sonnet/test/redux-saga/operators/takeEvery.test.ts for additional exemplification.

import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
,
import Fiber
Fiber
,
function pipe<A>(a: A): A (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

When to Use

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g)

becomes:

import { pipe, Array } from "effect"
pipe(as, Array.map(f), Array.filter(g))

Details

The pipe function is a utility that allows us to compose functions in a readable and sequential manner. It takes the output of one function and passes it as the input to the next function in the pipeline. This enables us to build complex transformations by chaining multiple functions together.

import { pipe } from "effect"
const result = pipe(input, func1, func2, ..., funcN)

In this syntax, input is the initial value, and func1, func2, ..., funcN are the functions to be applied in sequence. The result of each function becomes the input for the next function, and the final result is returned.

Here's an illustration of how pipe works:

┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌────────┐
│ input │───►│ func1 │───►│ func2 │───►│ ... │───►│ funcN │───►│ result │
└───────┘ └───────┘ └───────┘ └───────┘ └───────┘ └────────┘

It's important to note that functions passed to pipe must have a single argument because they are only called with a single argument.

@example

// Example: Chaining Arithmetic Operations
import { pipe } from "effect"
// Define simple arithmetic operations
const increment = (x: number) => x + 1
const double = (x: number) => x * 2
const subtractTen = (x: number) => x - 10
// Sequentially apply these operations using `pipe`
const result = pipe(5, increment, double, subtractTen)
console.log(result)
// Output: 2

@since2.0.0

pipe
} from "effect"
import {
import Sonnet
Sonnet
,
import Operators
Operators
} from "redux-sonnet"
declare const
const worker: (action: unknown) => Effect.Effect<void, never, never>
worker
: (
action: unknown
action
: unknown) =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0

@since2.0.0

Effect
<void, never, never>
const
const root: Effect.Effect<void, never, Sonnet.SonnetService>
root
=
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const gen: <YieldWrap<Effect.Effect<Fiber.RuntimeFiber<void, never>, never, Sonnet.SonnetService>>, void>(f: (resume: Effect.Adapter) => Generator<...>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

@example

import { Effect } from "effect"
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function* () {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@since2.0.0

gen
(function*(){
yield*
import Operators
Operators
.
const takeEvery: <[], never>(pred: Predicate<Action>, effect: (action: Action) => Effect.Effect<void, never, never>) => Effect.Effect<Fiber.RuntimeFiber<...>, never, Sonnet.SonnetService>

TODO: needs consideration of halting vs interruption behavior

@since0.0.0

takeEvery
(
import Operators
Operators
.
const ofType: <"ACTION">(type: "ACTION") => Refinement<unknown, Action<"ACTION">>

Filter by virtue of Action.type matching the given string.

@example

import { Stream } from "effect"
import { Operators } from "redux-sonnet"
const actions = Stream.make(
{ type: "A1" },
{ type: "A2" },
{ type: "A3" },
)
const filtered = actions.pipe(
Stream.filter(Operators.ofType("A2"))
)
// Effect.runPromise(Stream.runCollect(filtered)).then(console.log)
// { _id: 'Chunk', values: [ { type: "A2"} ] }

@since0.0.0

ofType
('ACTION'),
const worker: (action: unknown) => Effect.Effect<void, never, never>
worker
)
})
const
const sonnet: Sonnet.Sonnet<never, never>
sonnet
=
import Sonnet
Sonnet
.
const make: <Sonnet.SonnetService, never>(rootEffect: Effect.Effect<void, never, Sonnet.SonnetService>, layer: Layer<Sonnet.SonnetService, never, never>, memoMap?: MemoMap | undefined) => Sonnet.Sonnet<...>

Construct a redux-sonnet middleware.

NOTE: side-effect handlers are provided up-front to guarantee declarative behavior.

@example

import { configureStore, Tuple as RTKTuple } from "@reduxjs/toolkit"
import { Sonnet, Stanza } from "redux-sonnet"
import { Stream, Layer } from "effect"
const stanza = Stanza.fromStream(Stream.make(
{ type: "ACTION-1" },
{ type: "ACTION-2" },
{ type: "ACTION-3" },
))
const sonnet = Sonnet.make(
// ^?
stanza,
Sonnet.defaultLayer,
)
const store = configureStore({
// ^?
reducer: () => {},
middleware: () => new RTKTuple(sonnet)
})

@since0.0.0

make
(
const root: Effect.Effect<void, never, Sonnet.SonnetService>
root
,
import Sonnet
Sonnet
.
const defaultLayer: Layer<Sonnet.SonnetService, never, never>

@since0.0.0

defaultLayer
)
await
pipe<Fiber.RuntimeFiber<void, never>, Effect.Effect<Exit<void, never>, never, never>, Promise<Exit<void, never>>>(a: Fiber.RuntimeFiber<void, never>, ab: (a: Fiber.RuntimeFiber<...>) => Effect.Effect<...>, bc: (b: Effect.Effect<...>) => Promise<...>): Promise<...> (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

When to Use

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g)

becomes:

import { pipe, Array } from "effect"
pipe(as, Array.map(f), Array.filter(g))

Details

The pipe function is a utility that allows us to compose functions in a readable and sequential manner. It takes the output of one function and passes it as the input to the next function in the pipeline. This enables us to build complex transformations by chaining multiple functions together.

import { pipe } from "effect"
const result = pipe(input, func1, func2, ..., funcN)

In this syntax, input is the initial value, and func1, func2, ..., funcN are the functions to be applied in sequence. The result of each function becomes the input for the next function, and the final result is returned.

Here's an illustration of how pipe works:

┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌────────┐
│ input │───►│ func1 │───►│ func2 │───►│ ... │───►│ funcN │───►│ result │
└───────┘ └───────┘ └───────┘ └───────┘ └───────┘ └────────┘

It's important to note that functions passed to pipe must have a single argument because they are only called with a single argument.

@example

// Example: Chaining Arithmetic Operations
import { pipe } from "effect"
// Define simple arithmetic operations
const increment = (x: number) => x + 1
const double = (x: number) => x * 2
const subtractTen = (x: number) => x - 10
// Sequentially apply these operations using `pipe`
const result = pipe(5, increment, double, subtractTen)
console.log(result)
// Output: 2

@since2.0.0

pipe
(
const sonnet: Sonnet.Sonnet<never, never>
sonnet
.
Sonnet<never, never>.fiber: Fiber.RuntimeFiber<void, never>
fiber
,
import Fiber
Fiber
.
const interrupt: <A, E>(self: Fiber.Fiber<A, E>) => Effect.Effect<Exit<A, E>>

Interrupts the fiber from whichever fiber is calling this method. If the fiber has already exited, the returned effect will resume immediately. Otherwise, the effect will resume when the fiber exits.

@since2.0.0

interrupt
,
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const runPromise: <A, E>(effect: Effect.Effect<A, E, never>, options?: {
readonly signal?: AbortSignal;
} | undefined) => Promise<A>

Executes an effect and returns the result as a Promise.

When to Use

Use runPromise when you need to execute an effect and work with the result using Promise syntax, typically for compatibility with other promise-based code.

If the effect succeeds, the promise will resolve with the result. If the effect fails, the promise will reject with an error.

@seerunPromiseExit for a version that returns an Exit type instead of rejecting.

@example

// Title: Running a Successful Effect as a Promise
import { Effect } from "effect"
Effect.runPromise(Effect.succeed(1)).then(console.log)
// Output: 1

@example

//Example: Handling a Failing Effect as a Rejected Promise import { Effect } from "effect"

Effect.runPromise(Effect.fail("my error")).catch(console.error) // Output: // (FiberFailure) Error: my error

@since2.0.0

runPromise
,
)