diff --git a/packages/effect-lens/src/Lens.test.ts b/packages/effect-lens/src/Lens.test.ts index 208d74a..6d5613a 100644 --- a/packages/effect-lens/src/Lens.test.ts +++ b/packages/effect-lens/src/Lens.test.ts @@ -4,12 +4,12 @@ import * as Lens from "./Lens.js" describe("Lens", () => { - test("focusField focuses a nested property without touching other fields", async () => { + test("focusObjectField focuses a nested property without touching other fields", async () => { const [initialCount, updatedState] = await Effect.runPromise( Effect.flatMap( SubscriptionRef.make({ count: 1, label: "original" }), parent => { - const countLens = Lens.focusField(Lens.fromSubscriptionRef(parent), "count") + const countLens = Lens.focusObjectField(Lens.fromSubscriptionRef(parent), "count") return Effect.flatMap( Lens.get(countLens), count => Effect.flatMap( @@ -25,13 +25,13 @@ describe("Lens", () => { expect(updatedState).toEqual({ count: 6, label: "original" }) }) - test("focusMutableField preserves the root identity when mutating in place", async () => { + test("focusObjectMutableField preserves the root identity when mutating in place", async () => { const original = { detail: "keep" } const updated = await Effect.runPromise( Effect.flatMap( SubscriptionRef.make(original), parent => { - const detailLens = Lens.focusMutableField(Lens.fromSubscriptionRef(parent), "detail") + const detailLens = Lens.focusObjectMutableField(Lens.fromSubscriptionRef(parent), "detail") return Effect.flatMap( Lens.set(detailLens, "mutated"), () => parent.get, @@ -80,6 +80,42 @@ describe("Lens", () => { expect(updated).toEqual(["baz", "bar"]) }) + test("focusTupleAt updates the selected tuple index immutably", async () => { + const updated = await Effect.runPromise( + Effect.flatMap( + SubscriptionRef.make(["a", "b", "c"] as const), + parent => { + const elementLens = Lens.focusTupleAt(Lens.fromSubscriptionRef(parent), 1) + return Effect.flatMap( + Lens.set(elementLens, "updated"), + () => parent.get, + ) + }, + ), + ) + + expect(updated).toEqual(["a", "updated", "c"]) + }) + + test("focusMutableTupleAt mutates the tuple reference in place", async () => { + const original = ["foo", "bar"] as ["foo", "bar"] + const updated = await Effect.runPromise( + Effect.flatMap( + SubscriptionRef.make(original), + parent => { + const elementLens = Lens.focusMutableTupleAt(Lens.fromSubscriptionRef(parent), 0) + return Effect.flatMap( + Lens.set(elementLens, "baz"), + () => parent.get, + ) + }, + ), + ) + + expect(updated).toBe(original) + expect(updated).toEqual(["baz", "bar"]) + }) + test("focusChunkAt replaces the focused chunk element", async () => { const updated = await Effect.runPromise( Effect.flatMap( diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 4901de8..d7f7c6f 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -296,6 +296,49 @@ export const focusMutableArrayAt: { ), )) +/** + * Narrows the focus to an indexed element of a readonly tuple. Replaces the tuple in an immutable fashion when written to. + */ +export const focusTupleAt: { + ( + self: Lens, + index: number, + ): Lens + ( + index: number + ): (self: Lens) => Lens +} = Function.dual(2, ( + self: Lens, + index: number, +): Lens => mapEffect( + self, + Array.get(index), + (a, b) => Array.replaceOption(a, index, b) as any, +)) + +/** + * Narrows the focus to an indexed element of a mutable tuple. Mutates the tuple in place when written to. + */ +export const focusMutableTupleAt: { + ( + self: Lens, + index: number, + ): Lens + ( + index: number + ): (self: Lens) => Lens +} = Function.dual(2, ( + self: Lens, + index: number, +): Lens => mapEffect( + self, + Array.get(index), + (a, b) => Effect.flatMap( + Array.get(a, index), + () => Effect.as(Effect.sync(() => { a[index] = b }), a), + ), +)) + /** * Narrows the focus to an indexed element of `Chunk`. Replaces the `Chunk` in an immutable fashion when written to. */