Skip to content

Effect 3.16 (Release)

Effect 3.16 has been released! This release includes a number of new features and improvements. Here’s a summary of what’s new:

An ExecutionPlan can be used with Effect.withExecutionPlan or Stream.withExecutionPlan, allowing you to provide different resources for each step of execution until the effect succeeds or the plan is exhausted.

This can be useful for AI, when you want to fallback to alternative providers when the primary provider has downtime.

For example:

import { type AiLanguageModel } from "@effect/ai"
import type { Layer } from "effect"
import { Effect, ExecutionPlan, Schedule } from "effect"
declare const layerBad: Layer.Layer<AiLanguageModel.AiLanguageModel>
declare const layerGood: Layer.Layer<AiLanguageModel.AiLanguageModel>
const AiPlan = ExecutionPlan.make(
{
// First try with the bad layer 2 times with a 3 second delay between attempts
provide: layerBad,
attempts: 2,
schedule: Schedule.spaced(3000)
},
// Then try with the bad layer 3 times with a 1 second delay between attempts
{
provide: layerBad,
attempts: 3,
schedule: Schedule.spaced(1000)
},
// Finally try with the good layer.
//
// If `attempts` is omitted, the plan will only attempt once, unless a schedule is provided.
{
provide: layerGood
}
)
declare const effect: Effect.Effect<
void,
never,
AiLanguageModel.AiLanguageModel
>
const withPlan: Effect.Effect<void> = Effect.withExecutionPlan(
effect,
AiPlan
)

This allows you to pass parameters to the effect & scoped Effect.Service constructors, which will also be reflected in the .Default layer.

import type { Layer } from "effect"
import { Effect } from "effect"
class NumberService extends Effect.Service<NumberService>()(
"NumberService",
{
// You can now pass a function to the `effect` and `scoped` constructors
effect: Effect.fn(function* (input: number) {
return {
get: Effect.succeed(`The number is: ${input}`)
} as const
})
}
) {}
// Pass the arguments to the `Default` layer
const CoolNumberServiceLayer: Layer.Layer<NumberService> =
NumberService.Default(6942)

LayerMap has been simplified to directly return Layer’s, instead of using custom api’s to provide services.

Here is an example of the new usage pattern:

import { NodeRuntime } from "@effect/platform-node"
import { Context, Effect, FiberRef, Layer, LayerMap } from "effect"
class Greeter extends Context.Tag("Greeter")<
Greeter,
{
greet: Effect.Effect<string>
}
>() {}
// create a service that wraps a LayerMap
class GreeterMap extends LayerMap.Service<GreeterMap>()("GreeterMap", {
// define the lookup function for the layer map
//
// The returned Layer will be used to provide the Greeter service for the
// given name.
lookup: (name: string) =>
Layer.succeed(Greeter, {
greet: Effect.succeed(`Hello, ${name}!`)
}),
// If a layer is not used for a certain amount of time, it can be removed
idleTimeToLive: "5 seconds",
// Supply the dependencies for the layers in the LayerMap
dependencies: []
}) {}
// usage
const program: Effect.Effect<void, never, GreeterMap> = Effect.gen(
function* () {
// access and use the Greeter service
const greeter = yield* Greeter
yield* Effect.log(yield* greeter.greet)
}
).pipe(
// use the GreeterMap service to provide a variant of the Greeter service
Effect.provide(GreeterMap.get("John"))
)
// run the program
program.pipe(Effect.provide(GreeterMap.Default), NodeRuntime.runMain)

Schedule.CurrentIterationMetadata allows you to access metadata for the current Schedule iteration. For instance, when inside a Effect.repeat or Effect.retry region.

import { Effect, Schedule } from "effect"
Effect.gen(function* () {
// You can now access the following information when inside a Schedule execution.
//
// {
// elapsed: Duration.zero,
// elapsedSincePrevious: Duration.zero,
// input: undefined,
// now: 0,
// recurrence: 2,
// start: 0
// }
const currentIterationMetadata =
yield* Schedule.CurrentIterationMetadata
}).pipe(Effect.repeat(Schedule.recurs(2)))

New Config apis have been added, to make it easier to work with network ports and branded types.

import { Brand, Config } from "effect"
// ensures that the value is a valid port number
const dbPort: Config.Config<number> = Config.port("DB_PORT")
import { Brand, Config } from "effect"
type Port = Brand.Branded<number, "Port">
const Port = Brand.refined<Port>(
(num) =>
!Number.isNaN(num) &&
Number.isInteger(num) &&
num >= 1 &&
num <= 65535,
(n) => Brand.error(`Expected ${n} to be an TCP port`)
)
const dbPort: Config.Config<Port> = Config.number("DB_PORT").pipe(
// refine the config value using a brand constructor
Config.branded(Port)
)
  • Array / Iterable.countBy - count the elements that match the given predicate
  • Array / Chunk.removeOption - remove an element at a given index, returning an Option depending on success
  • HashMap.hasBy - check if a HashMap contains a member using a predicate
  • BigDecimal rounding apis have been added

There were several other smaller changes made. Take a look through the CHANGELOG to see them all: CHANGELOG.

Don’t forget to join our Discord Community to follow the last updates and discuss every tiny detail!