0.1.2 #1

Merged
Thilawyn merged 8 commits from next into master 2026-03-27 14:32:05 +01:00
2 changed files with 83 additions and 4 deletions
Showing only changes of commit b2cf51c393 - Show all commits

View File

@@ -4,12 +4,12 @@ import * as Lens from "./Lens.js"
describe("Lens", () => { 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( const [initialCount, updatedState] = await Effect.runPromise(
Effect.flatMap( Effect.flatMap(
SubscriptionRef.make({ count: 1, label: "original" }), SubscriptionRef.make({ count: 1, label: "original" }),
parent => { parent => {
const countLens = Lens.focusField(Lens.fromSubscriptionRef(parent), "count") const countLens = Lens.focusObjectField(Lens.fromSubscriptionRef(parent), "count")
return Effect.flatMap( return Effect.flatMap(
Lens.get(countLens), Lens.get(countLens),
count => Effect.flatMap( count => Effect.flatMap(
@@ -25,13 +25,13 @@ describe("Lens", () => {
expect(updatedState).toEqual({ count: 6, label: "original" }) 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 original = { detail: "keep" }
const updated = await Effect.runPromise( const updated = await Effect.runPromise(
Effect.flatMap( Effect.flatMap(
SubscriptionRef.make(original), SubscriptionRef.make(original),
parent => { parent => {
const detailLens = Lens.focusMutableField(Lens.fromSubscriptionRef(parent), "detail") const detailLens = Lens.focusObjectMutableField(Lens.fromSubscriptionRef(parent), "detail")
return Effect.flatMap( return Effect.flatMap(
Lens.set(detailLens, "mutated"), Lens.set(detailLens, "mutated"),
() => parent.get, () => parent.get,
@@ -80,6 +80,42 @@ describe("Lens", () => {
expect(updated).toEqual(["baz", "bar"]) 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 () => { test("focusChunkAt replaces the focused chunk element", async () => {
const updated = await Effect.runPromise( const updated = await Effect.runPromise(
Effect.flatMap( Effect.flatMap(

View File

@@ -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: {
<T extends readonly [any, ...any[]], ER, EW, RR, RW>(
self: Lens<T, ER, EW, RR, RW>,
index: number,
): Lens<T[number], ER | NoSuchElementException, EW | NoSuchElementException, RR, RW>
<T extends readonly [any, ...any[]], ER, EW, RR, RW>(
index: number
): (self: Lens<T, ER, EW, RR, RW>) => Lens<T[number], ER | NoSuchElementException, EW | NoSuchElementException, RR, RW>
} = Function.dual(2, <T extends readonly [any, ...any[]], ER, EW, RR, RW>(
self: Lens<T, ER, EW, RR, RW>,
index: number,
): Lens<T[number], ER | NoSuchElementException, EW | NoSuchElementException, RR, RW> => 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: {
<T extends [any, ...any[]], ER, EW, RR, RW>(
self: Lens<T, ER, EW, RR, RW>,
index: number,
): Lens<T[number], ER | NoSuchElementException, EW | NoSuchElementException, RR, RW>
<T extends [any, ...any[]], ER, EW, RR, RW>(
index: number
): (self: Lens<T, ER, EW, RR, RW>) => Lens<T[number], ER | NoSuchElementException, EW | NoSuchElementException, RR, RW>
} = Function.dual(2, <T extends [any, ...any[]], ER, EW, RR, RW>(
self: Lens<T, ER, EW, RR, RW>,
index: number,
): Lens<T[number], ER | NoSuchElementException, EW | NoSuchElementException, RR, RW> => 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. * Narrows the focus to an indexed element of `Chunk`. Replaces the `Chunk` in an immutable fashion when written to.
*/ */