From 4c45f49092fdde0ef5c4ffc492ce15520f59b54a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sat, 16 May 2026 15:12:29 +0200 Subject: [PATCH] Add provide --- packages/effect-lens/README.md | 14 +++++++++++ packages/effect-lens/src/Lens.test.ts | 34 ++++++++++++++++++++++++++- packages/effect-lens/src/Lens.ts | 27 ++++++++++++++++++++- 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/packages/effect-lens/README.md b/packages/effect-lens/README.md index c976d80..d177c27 100644 --- a/packages/effect-lens/README.md +++ b/packages/effect-lens/README.md @@ -95,6 +95,20 @@ const lens = Effect.all([ Note: while Lens supports asynchronous effects for the proxy logic, we would recommend keeping them synchronous to preserve atomicity. +If a `Lens` depends on a service in its environment, you can provide that service directly to the lens: +```typescript +class Offset extends Context.Tag("Offset")() {} + +const root = Lens.fromSubscriptionRef(ref) +const offsetLens = Lens.mapEffect( + root, + n => Effect.map(Offset, ({ value }) => n + value), + (_n, next) => Effect.map(Offset, ({ value }) => next - value), +) + +const runnableLens = Lens.provide(offsetLens, Offset, { value: 5 }) +``` + ### Focusing diff --git a/packages/effect-lens/src/Lens.test.ts b/packages/effect-lens/src/Lens.test.ts index fabe248..e417b42 100644 --- a/packages/effect-lens/src/Lens.test.ts +++ b/packages/effect-lens/src/Lens.test.ts @@ -1,9 +1,11 @@ import { describe, expect, test } from "bun:test" -import { Chunk, Effect, Option, SubscriptionRef } from "effect" +import { Chunk, Context, Effect, Option, SubscriptionRef } from "effect" import * as Lens from "./Lens.js" describe("Lens", () => { + class Offset extends Context.Tag("Offset")() {} + test("mapOption transforms Some values and preserves None", async () => { const result = await Effect.runPromise( Effect.flatMap( @@ -54,6 +56,36 @@ describe("Lens", () => { expect(result[1]).toEqual(Option.some(50)) // 100 / 2 }) + test("provide supplies a service to get and modify", async () => { + const result = await Effect.runPromise( + Effect.flatMap( + SubscriptionRef.make(10), + parent => { + const lens = Lens.provide( + Lens.mapEffect( + Lens.fromSubscriptionRef(parent), + n => Effect.map(Offset, ({ value }) => n + value), + (_n, next) => Effect.map(Offset, ({ value }) => next - value), + ), + Offset, + { value: 5 }, + ) + + return Effect.flatMap( + Lens.get(lens), + value => Effect.flatMap( + Lens.set(lens, 30), + () => Effect.map(parent.get, parentValue => [value, parentValue] as const), + ), + ) + }, + ), + ) + + expect(result[0]).toBe(15) + expect(result[1]).toBe(25) + }) + test("focusObjectOn focuses a nested property without touching other fields", async () => { const [initialCount, updatedState] = await Effect.runPromise( Effect.flatMap( diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index ca97fd6..9c6663b 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -1,4 +1,4 @@ -import { Array, Chunk, Effect, Function, identity, Option, Pipeable, Predicate, Readable, Stream, type SubscriptionRef, type SynchronizedRef } from "effect" +import { Array, Chunk, type Context, Effect, Function, identity, Option, Pipeable, Predicate, Readable, Stream, type SubscriptionRef, type SynchronizedRef } from "effect" import type { NoSuchElementException } from "effect/Cause" import * as Subscribable from "./Subscribable.js" @@ -269,6 +269,31 @@ export const mapStream: { get modify() { return self.modify }, })) +/** + * Provides a single service to a `Lens`, removing it from both the read and write environments. + */ +export const provide: { + ( + self: Lens, + tag: Context.Tag, + service: NoInfer, + ): Lens, Exclude> + ( + tag: Context.Tag, + service: NoInfer, + ): (self: Lens) => Lens, Exclude> +} = Function.dual(3, ( + self: Lens, + tag: Context.Tag, + service: NoInfer, +): Lens, Exclude> => make({ + get get() { return Effect.provideService(self.get, tag, service) }, + get changes() { return Stream.provideService(self.changes, tag, service) }, + modify: ( + f: (a: A) => Effect.Effect + ) => Effect.provideService(self.modify(f), tag, service), +})) + /** * Narrows the focus to a field of an object. Replaces the object in an immutable fashion when written to.