Skip to content

Effect 3.16 (Release)

New Effect release featuring the new ExecutionPlan module, Effect.Service & LayerMap improvements, and more!


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

ExecutionPlan module

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)

Effect.Service parameter support

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 improvements

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

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)))

Config.port & Config.branded

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),
)

Other changes

  • 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!

Share

Last updated

// Effect Community

Join the conversation on Discord

Meet engineers running Effect in production.