Compare commits
51 Commits
6b1f6091d1
...
cc56a1d338
| Author | SHA1 | Date | |
|---|---|---|---|
| cc56a1d338 | |||
|
|
13a12f5938 | ||
|
|
a2f3a07834 | ||
|
|
8fb997a2a0 | ||
|
|
ff72c83ef0 | ||
|
|
80c434d390 | ||
|
|
f1d0771356 | ||
|
|
2646e295d9 | ||
|
|
45bf604381 | ||
|
|
6fa34069ea | ||
|
|
c338682bf2 | ||
|
|
087317171a | ||
|
|
f08cc59fef | ||
|
|
34b9452c1c | ||
|
|
74dd87f4ea | ||
|
|
6e939884cc | ||
|
|
8c86c1ce76 | ||
|
|
e175eac701 | ||
|
|
7f18fc5553 | ||
|
|
3ff4e8758a | ||
|
|
4ae32fce49 | ||
|
|
1a25214984 | ||
|
|
580c6ec3d3 | ||
|
|
c6db61c258 | ||
|
|
eea6bcac4d | ||
|
|
ef1de00020 | ||
|
|
99f5e089f5 | ||
|
|
8430b4ddf6 | ||
|
|
88ad7cb1ac | ||
|
|
11d23aa10c | ||
|
|
3f05a5099e | ||
|
|
a30c527803 | ||
|
|
10d69f977b | ||
|
|
7f8411e83c | ||
|
|
e89babe223 | ||
|
|
aab613030d | ||
|
|
84bf50032b | ||
|
|
b8ad8a94c9 | ||
|
|
99bdd6a3ec | ||
|
|
64d6c20d06 | ||
|
|
285fc84275 | ||
|
|
821ba95247 | ||
|
|
45c854a8d0 | ||
|
|
54b05ed8da | ||
|
|
0cb85adca0 | ||
|
|
7f57e034e4 | ||
|
|
4f4dde988a | ||
|
|
ba911ad598 | ||
|
|
672225d037 | ||
|
|
39125943ec | ||
|
|
bba8d838b7 |
4
bun.lock
4
bun.lock
@@ -6,7 +6,7 @@
|
||||
"name": "@effect-fc/monorepo",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.11",
|
||||
"@effect/language-service": "^0.80.0",
|
||||
"@effect/language-service": "^0.83.0",
|
||||
"@types/bun": "^1.3.6",
|
||||
"npm-check-updates": "^19.3.1",
|
||||
"npm-sort": "^0.0.4",
|
||||
@@ -503,7 +503,7 @@
|
||||
|
||||
"@effect-fc/example": ["@effect-fc/example@workspace:packages/example"],
|
||||
|
||||
"@effect/language-service": ["@effect/language-service@0.80.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-dKMATT1fDzaCpNrICpXga7sjJBtFLpKCAoE/1MiGXI8UwcHA9rmAZ2t52JO9g/kJpERWyomkJ+rl+VFlwNIofg=="],
|
||||
"@effect/language-service": ["@effect/language-service@0.83.1", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-cBQ7yg/P4+kAp+JFX+9Bk5bc1jG3UMVq8pgtW5VAWBl/yiTjWlCGMKC4UxX2KlpREvjTkvS8BKi1wYw0phWmAQ=="],
|
||||
|
||||
"@effect/platform": ["@effect/platform@0.94.5", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.17" } }, "sha512-z05APUiDDPbodhTkH/RJqOLoCU11bU2IZLfcwLFrld03+ob1VeqRnELQlmueLIYm6NZifHAtjl32V+GRt34y4A=="],
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.11",
|
||||
"@effect/language-service": "^0.80.0",
|
||||
"@effect/language-service": "^0.83.0",
|
||||
"@types/bun": "^1.3.6",
|
||||
"npm-check-updates": "^19.3.1",
|
||||
"npm-sort": "^0.0.4",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"clean:modules": "rm -rf node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-browser": "^0.74.0"
|
||||
"@effect/platform-browser": "^0.76.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0",
|
||||
|
||||
139
packages/effect-fc/src/Lens.test.ts
Normal file
139
packages/effect-fc/src/Lens.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Chunk, Effect, SubscriptionRef } from "effect"
|
||||
import * as Lens from "./Lens.js"
|
||||
|
||||
|
||||
describe("Lens", () => {
|
||||
test("focusField 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")
|
||||
return Effect.flatMap(
|
||||
Lens.get(countLens),
|
||||
count => Effect.flatMap(
|
||||
Lens.set(countLens, count + 5),
|
||||
() => Effect.map(parent.get, state => [count, state] as const),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(initialCount).toBe(1)
|
||||
expect(updatedState).toEqual({ count: 6, label: "original" })
|
||||
})
|
||||
|
||||
test("focusMutableField 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")
|
||||
return Effect.flatMap(
|
||||
Lens.set(detailLens, "mutated"),
|
||||
() => parent.get,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(updated).toBe(original)
|
||||
expect(updated.detail).toBe("mutated")
|
||||
})
|
||||
|
||||
test("focusArrayAt updates the selected index", async () => {
|
||||
const updated = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make([10, 20, 30]),
|
||||
parent => {
|
||||
const elementLens = Lens.focusArrayAt(Lens.fromSubscriptionRef(parent), 1)
|
||||
return Effect.flatMap(
|
||||
Lens.update(elementLens, value => value + 5),
|
||||
() => parent.get,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(updated).toEqual([10, 25, 30])
|
||||
})
|
||||
|
||||
test("focusMutableArrayAt mutates the array reference in place", async () => {
|
||||
const original = ["foo", "bar"]
|
||||
const updated = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make(original),
|
||||
parent => {
|
||||
const elementLens = Lens.focusMutableArrayAt(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(
|
||||
SubscriptionRef.make(Chunk.make(1, 2, 3) as Chunk.Chunk<number>),
|
||||
parent => {
|
||||
const elementLens = Lens.focusChunkAt(Lens.fromSubscriptionRef(parent), 2)
|
||||
return Effect.flatMap(
|
||||
Lens.set(elementLens, 99),
|
||||
() => parent.get,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(Chunk.toReadonlyArray(updated)).toEqual([1, 2, 99])
|
||||
})
|
||||
|
||||
// test("changes stream emits updates when lens mutates state", async () => {
|
||||
// const events = await Effect.runPromise(
|
||||
// Effect.flatMap(
|
||||
// SubscriptionRef.make({ count: 0 }),
|
||||
// parent => {
|
||||
// const lens = Lens.mapField(Lens.fromSubscriptionRef(parent), "count")
|
||||
// return Effect.fork(Stream.runCollect(Stream.take(lens.changes, 2))).pipe(
|
||||
// Effect.tap(Lens.set(lens, 1)),
|
||||
// Effect.tap(Lens.set(lens, 1)),
|
||||
// Effect.andThen(Fiber.join),
|
||||
// Effect.map(Chunk.toReadonlyArray),
|
||||
// )
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
|
||||
// expect(events).toEqual([1, 2])
|
||||
// })
|
||||
|
||||
// test("mapped changes stream can derive transformed values", async () => {
|
||||
// const derived = await Effect.runPromise(
|
||||
// Effect.flatMap(
|
||||
// SubscriptionRef.make({ count: 10 }),
|
||||
// parent => {
|
||||
// const lens = Lens.mapField(Lens.fromSubscriptionRef(parent), "count")
|
||||
// const transformed = Stream.map(lens.changes, count => `count:${ count }`)
|
||||
// return Effect.scoped(() => Effect.flatMap(
|
||||
// Effect.forkScoped(Stream.runCollect(Stream.take(transformed, 1))),
|
||||
// fiber => Effect.flatMap(
|
||||
// Lens.set(lens, 42),
|
||||
// () => Effect.join(fiber),
|
||||
// ),
|
||||
// ))
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
|
||||
// expect(derived).toEqual(["count:42"])
|
||||
// })
|
||||
})
|
||||
408
packages/effect-fc/src/Lens.ts
Normal file
408
packages/effect-fc/src/Lens.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
import { Array, Chunk, Effect, Function, Pipeable, Predicate, Readable, Stream, Subscribable, type SubscriptionRef } from "effect"
|
||||
import type { NoSuchElementException } from "effect/Cause"
|
||||
|
||||
|
||||
export const LensTypeId: unique symbol = Symbol.for("@effect-fc/Lens/Lens")
|
||||
export type LensTypeId = typeof LensTypeId
|
||||
|
||||
/**
|
||||
* A bidirectional view into some shared state that exposes:
|
||||
*
|
||||
* 1. a `get` effect for reading the current value of type `A`,
|
||||
* 2. a `changes` stream that emits every subsequent update to `A`, and
|
||||
* 3. a `modify` effect that can transform the current value.
|
||||
*/
|
||||
export interface Lens<in out A, in out ER = never, in out EW = never, in out RR = never, in out RW = never>
|
||||
extends Subscribable.Subscribable<A, ER, RR> {
|
||||
readonly [LensTypeId]: LensTypeId
|
||||
|
||||
readonly modify: <B, E1 = never, R1 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
|
||||
) => Effect.Effect<B, ER | EW | E1, RR | RW | R1>
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal `Lens` implementation.
|
||||
*/
|
||||
export class LensImpl<in out A, in out ER = never, in out EW = never, in out RR = never, in out RW = never>
|
||||
extends Pipeable.Class() implements Lens<A, ER, EW, RR, RW> {
|
||||
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
||||
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
||||
readonly [LensTypeId]: LensTypeId = LensTypeId
|
||||
|
||||
constructor(
|
||||
readonly get: Effect.Effect<A, ER, RR>,
|
||||
readonly changes: Stream.Stream<A, ER, RR>,
|
||||
readonly modify: <B, E1 = never, R1 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
|
||||
) => Effect.Effect<B, ER | EW | E1, RR | RW | R1>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether a value is a `Lens`.
|
||||
*/
|
||||
export const isLens = (u: unknown): u is Lens<unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, LensTypeId)
|
||||
|
||||
|
||||
/**
|
||||
* Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations.
|
||||
*
|
||||
* Either `modify` or `set` needs to be supplied.
|
||||
*/
|
||||
export const make = <A, ER, EW, RR, RW>(
|
||||
options: {
|
||||
readonly get: Effect.Effect<A, ER, RR>
|
||||
readonly changes: Stream.Stream<A, ER, RR>
|
||||
} & (
|
||||
| {
|
||||
readonly modify: <B, E1 = never, R1 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
|
||||
) => Effect.Effect<B, ER | EW | E1, RR | RW | R1>
|
||||
}
|
||||
| { readonly set: (a: A) => Effect.Effect<void, EW, RW> }
|
||||
)
|
||||
): Lens<A, ER, EW, RR, RW> => new LensImpl<A, ER, EW, RR, RW>(
|
||||
options.get,
|
||||
options.changes,
|
||||
Predicate.hasProperty(options, "modify")
|
||||
? options.modify
|
||||
: <B, E1 = never, R1 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
|
||||
) => Effect.flatMap(
|
||||
options.get,
|
||||
a => Effect.flatMap(f(a), ([b, next]) => Effect.as(options.set(next), b)
|
||||
)),
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a `Lens` that proxies a `SubscriptionRef`.
|
||||
*/
|
||||
export const fromSubscriptionRef = <A>(
|
||||
ref: SubscriptionRef.SubscriptionRef<A>
|
||||
): Lens<A, never, never, never, never> => make({
|
||||
get get() { return ref.get },
|
||||
get changes() { return ref.changes },
|
||||
modify: <B, E1 = never, R1 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E1, R1>
|
||||
) => ref.modifyEffect(f),
|
||||
})
|
||||
|
||||
/**
|
||||
* Flattens an effectful `Lens`.
|
||||
*/
|
||||
export const unwrap = <A, ER, EW, RR, RW, E1, R1>(
|
||||
effect: Effect.Effect<Lens<A, ER, EW, RR, RW>, E1, R1>
|
||||
): Lens<A, ER | E1, EW | E1, RR | R1, RW | R1> => make({
|
||||
get: Effect.flatMap(effect, l => l.get),
|
||||
changes: Stream.unwrap(Effect.map(effect, l => l.changes)),
|
||||
modify: <B, E2 = never, R2 = never>(
|
||||
f: (a: A) => Effect.Effect<readonly [B, A], E2, R2>
|
||||
) => Effect.flatMap(effect, l => l.modify(f)),
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Derives a new `Lens` by applying synchronous getters and setters over the focused value.
|
||||
*/
|
||||
export const map: {
|
||||
<A, ER, EW, RR, RW, B>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
get: (a: NoInfer<A>) => B,
|
||||
set: (a: NoInfer<A>, b: B) => NoInfer<A>,
|
||||
): Lens<B, ER, EW, RR, RW>
|
||||
<A, ER, EW, RR, RW, B>(
|
||||
get: (a: NoInfer<A>) => B,
|
||||
set: (a: NoInfer<A>, b: B) => NoInfer<A>,
|
||||
): (self: Lens<A, ER, EW, RR, RW>) => Lens<B, ER, EW, RR, RW>
|
||||
} = Function.dual(3, <A, ER, EW, RR, RW, B>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
get: (a: NoInfer<A>) => B,
|
||||
set: (a: NoInfer<A>, b: B) => NoInfer<A>,
|
||||
): Lens<B, ER, EW, RR, RW> => make({
|
||||
get get() { return Effect.map(self.get, get) },
|
||||
get changes() { return Stream.map(self.changes, get) },
|
||||
modify: <C, E1 = never, R1 = never>(
|
||||
f: (b: B) => Effect.Effect<readonly [C, B], E1, R1>
|
||||
) => self.modify(a =>
|
||||
Effect.flatMap(f(get(a)), ([c, next]) => Effect.succeed([c, set(a, next)]))
|
||||
),
|
||||
}))
|
||||
|
||||
/**
|
||||
* Derives a new `Lens` by applying effectful getters and setters over the focused value.
|
||||
*/
|
||||
export const mapEffect: {
|
||||
<A, ER, EW, RR, RW, B, EGet = never, RGet = never, ESet = never, RSet = never>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
get: (a: NoInfer<A>) => Effect.Effect<B, EGet, RGet>,
|
||||
set: (a: NoInfer<A>, b: B) => Effect.Effect<NoInfer<A>, ESet, RSet>,
|
||||
): Lens<B, ER | EGet, EW | ESet, RR | RGet, RW | RSet>
|
||||
<A, ER, EW, RR, RW, B, EGet = never, RGet = never, ESet = never, RSet = never>(
|
||||
get: (a: NoInfer<A>) => Effect.Effect<B, EGet, RGet>,
|
||||
set: (a: NoInfer<A>, b: B) => Effect.Effect<NoInfer<A>, ESet, RSet>,
|
||||
): (self: Lens<A, ER, EW, RR, RW>) => Lens<B, ER | EGet, EW | ESet, RR | RGet, RW | RSet>
|
||||
} = Function.dual(3, <A, ER, EW, RR, RW, B, EGet = never, RGet = never, ESet = never, RSet = never>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
get: (a: NoInfer<A>) => Effect.Effect<B, EGet, RGet>,
|
||||
set: (a: NoInfer<A>, b: B) => Effect.Effect<NoInfer<A>, ESet, RSet>,
|
||||
): Lens<B, ER | EGet, EW | ESet, RR | RGet, RW | RSet> => make({
|
||||
get get() { return Effect.flatMap(self.get, get) },
|
||||
get changes() { return Stream.mapEffect(self.changes, get) },
|
||||
modify: <C, E1 = never, R1 = never>(
|
||||
f: (b: B) => Effect.Effect<readonly [C, B], E1, R1>
|
||||
) => self.modify(a => Effect.flatMap(
|
||||
get(a),
|
||||
b => Effect.flatMap(
|
||||
f(b),
|
||||
([c, bNext]) => Effect.flatMap(
|
||||
set(a, bNext),
|
||||
nextA => Effect.succeed([c, nextA] as const),
|
||||
),
|
||||
)
|
||||
)),
|
||||
}))
|
||||
|
||||
/**
|
||||
* Allows transforming only the `changes` stream of a `Lens` while keeping the focus type intact.
|
||||
*/
|
||||
export const mapStream: {
|
||||
<A, ER, EW, RR, RW>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (changes: Stream.Stream<NoInfer<A>, NoInfer<ER>, NoInfer<RR>>) => Stream.Stream<NoInfer<A>, NoInfer<ER>, NoInfer<RR>>,
|
||||
): Lens<A, ER, EW, RR, RW>
|
||||
<A, ER, EW, RR, RW>(
|
||||
f: (changes: Stream.Stream<NoInfer<A>, NoInfer<ER>, NoInfer<RR>>) => Stream.Stream<NoInfer<A>, NoInfer<ER>, NoInfer<RR>>,
|
||||
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A, ER, EW, RR, RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
f: (changes: Stream.Stream<NoInfer<A>, NoInfer<ER>, NoInfer<RR>>) => Stream.Stream<NoInfer<A>, NoInfer<ER>, NoInfer<RR>>,
|
||||
): Lens<A, ER, EW, RR, RW> => make({
|
||||
get get() { return self.get },
|
||||
get changes() { return f(self.changes) },
|
||||
modify: self.modify,
|
||||
}))
|
||||
|
||||
|
||||
/**
|
||||
* Narrows the focus to a field of an object. Replaces the object in an immutable fashion when written to.
|
||||
*/
|
||||
export const focusField: {
|
||||
<A extends object, K extends keyof A, ER, EW, RR, RW>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
key: K,
|
||||
): Lens<A[K], ER, EW, RR, RW>
|
||||
<A extends object, K extends keyof A, ER, EW, RR, RW>(
|
||||
key: K,
|
||||
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A[K], ER, EW, RR, RW>
|
||||
} = Function.dual(2, <A extends object, K extends keyof A, ER, EW, RR, RW>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
key: K,
|
||||
): Lens<A[K], ER, EW, RR, RW> => map(
|
||||
self,
|
||||
a => a[key],
|
||||
(a, b) => Object.setPrototypeOf({ ...a, [key]: b }, Object.getPrototypeOf(a)),
|
||||
))
|
||||
|
||||
export declare namespace focusMutableField {
|
||||
export type WritableKeys<T> = {
|
||||
[K in keyof T]-?: IfEquals<
|
||||
{ [P in K]: T[K] },
|
||||
{ -readonly [P in K]: T[K] },
|
||||
K,
|
||||
never
|
||||
>
|
||||
}[keyof T]
|
||||
|
||||
type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? A : B
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrows the focus to a mutable field of an object. Mutates the object in place when written to.
|
||||
*/
|
||||
export const focusMutableField: {
|
||||
<A extends object, K extends focusMutableField.WritableKeys<A>, ER, EW, RR, RW>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
key: K,
|
||||
): Lens<A[K], ER, EW, RR, RW>
|
||||
<A extends object, K extends focusMutableField.WritableKeys<A>, ER, EW, RR, RW>(
|
||||
key: K,
|
||||
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A[K], ER, EW, RR, RW>
|
||||
} = Function.dual(2, <A extends object, K extends focusMutableField.WritableKeys<A>, ER, EW, RR, RW>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
key: K,
|
||||
): Lens<A[K], ER, EW, RR, RW> => map(self, a => a[key], (a, b) => { a[key] = b; return a }))
|
||||
|
||||
/**
|
||||
* Narrows the focus to an indexed element of an array. Replaces the array in an immutable fashion when written to.
|
||||
*/
|
||||
export const focusArrayAt: {
|
||||
<A extends readonly any[], ER, EW, RR, RW>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
index: number,
|
||||
): Lens<A[number]>
|
||||
<A extends readonly any[], ER, EW, RR, RW>(
|
||||
index: number
|
||||
): (self: Lens<A, ER, EW, RR, RW>) => Lens<A[number], ER | NoSuchElementException, EW | NoSuchElementException, RR, RW>
|
||||
} = Function.dual(2, <A extends readonly any[], ER, EW, RR, RW>(
|
||||
self: Lens<A, ER, EW, RR, RW>,
|
||||
index: number,
|
||||
): Lens<A[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 array. Mutates the array in place when written to.
|
||||
*/
|
||||
export const focusMutableArrayAt: {
|
||||
<A, ER, EW, RR, RW>(
|
||||
self: Lens<A[], ER, EW, RR, RW>,
|
||||
index: number,
|
||||
): Lens<A, ER | NoSuchElementException, EW | NoSuchElementException, RR, RW>
|
||||
<A, ER, EW, RR, RW>(
|
||||
index: number
|
||||
): (self: Lens<A[], ER, EW, RR, RW>) => Lens<A, ER | NoSuchElementException, EW | NoSuchElementException, RR, RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(
|
||||
self: Lens<A[], ER, EW, RR, RW>,
|
||||
index: number,
|
||||
): Lens<A, 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.
|
||||
*/
|
||||
export const focusChunkAt: {
|
||||
<A, ER, EW, RR, RW>(
|
||||
self: Lens<Chunk.Chunk<A>, ER, EW, RR, RW>,
|
||||
index: number,
|
||||
): Lens<A, ER | NoSuchElementException, EW, RR, RW>
|
||||
<A, ER, EW, RR, RW>(
|
||||
index: number
|
||||
): (self: Lens<Chunk.Chunk<A>, ER, EW, RR, RW>) => Lens<A, ER | NoSuchElementException, EW, RR, RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(
|
||||
self: Lens<Chunk.Chunk<A>, ER, EW, RR, RW>,
|
||||
index: number,
|
||||
): Lens<A, ER | NoSuchElementException, EW, RR, RW> => mapEffect(
|
||||
self,
|
||||
Chunk.get(index),
|
||||
(a, b) => Effect.succeed(Chunk.replace(a, index, b))),
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* Reads the current value from a `Lens`.
|
||||
*/
|
||||
export const get = <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>): Effect.Effect<A, ER, RR> => self.get
|
||||
|
||||
/**
|
||||
* Sets the value of a `Lens`.
|
||||
*/
|
||||
export const set: {
|
||||
<A, ER, EW, RR, RW>(value: A): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<void, ER | EW, RR | RW>
|
||||
<A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, value: A): Effect.Effect<void, ER | EW, RR | RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, value: A) =>
|
||||
self.modify<void, never, never>(() => Effect.succeed([void 0, value] as const)),
|
||||
)
|
||||
|
||||
/**
|
||||
* Sets a `Lens` to a new value and returns the previous value.
|
||||
*/
|
||||
export const getAndSet: {
|
||||
<A, ER, EW, RR, RW>(value: A): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<A, ER | EW, RR | RW>
|
||||
<A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, value: A): Effect.Effect<A, ER | EW, RR | RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, value: A) =>
|
||||
self.modify<A, never, never>(a => Effect.succeed([a, value] as const)),
|
||||
)
|
||||
|
||||
/**
|
||||
* Applies a synchronous transformation to the value of a `Lens`, discarding the previous value.
|
||||
*/
|
||||
export const update: {
|
||||
<A, ER, EW, RR, RW>(f: (a: A) => A): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<void, ER | EW, RR | RW>
|
||||
<A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => A): Effect.Effect<void, ER | EW, RR | RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => A) =>
|
||||
self.modify<void, never, never>(a => Effect.succeed([void 0, f(a)] as const)),
|
||||
)
|
||||
|
||||
/**
|
||||
* Applies an effectful transformation to the value of a `Lens`, discarding the previous value.
|
||||
*/
|
||||
export const updateEffect: {
|
||||
<A, ER, EW, RR, RW, E, R>(f: (a: A) => Effect.Effect<A, E, R>): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<void, ER | EW | E, RR | RW | R>
|
||||
<A, ER, EW, RR, RW, E, R>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => Effect.Effect<A, E, R>): Effect.Effect<void, ER | EW | E, RR | RW | R>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW, E, R>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => Effect.Effect<A, E, R>) =>
|
||||
self.modify<void, E, R>(a => Effect.flatMap(
|
||||
f(a),
|
||||
next => Effect.succeed([void 0, next] as const),
|
||||
)),
|
||||
)
|
||||
|
||||
/**
|
||||
* Applies a synchronous transformation the value of a `Lens` while returning the previous value.
|
||||
*/
|
||||
export const getAndUpdate: {
|
||||
<A, ER, EW, RR, RW>(f: (a: A) => A): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<A, ER | EW, RR | RW>
|
||||
<A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => A): Effect.Effect<A, ER | EW, RR | RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => A) =>
|
||||
self.modify<A, never, never>(a => Effect.succeed([a, f(a)] as const)),
|
||||
)
|
||||
|
||||
/**
|
||||
* Applies an effectful transformation the value of a `Lens` while returning the previous value.
|
||||
*/
|
||||
export const getAndUpdateEffect: {
|
||||
<A, ER, EW, RR, RW, E, R>(f: (a: A) => Effect.Effect<A, E, R>): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<A, ER | EW | E, RR | RW | R>
|
||||
<A, ER, EW, RR, RW, E, R>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => Effect.Effect<A, E, R>): Effect.Effect<A, ER | EW | E, RR | RW | R>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW, E, R>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => Effect.Effect<A, E, R>) =>
|
||||
self.modify<A, E, R>(a => Effect.flatMap(
|
||||
f(a),
|
||||
next => Effect.succeed([a, next] as const)
|
||||
)),
|
||||
)
|
||||
|
||||
/**
|
||||
* Sets the value of a `Lens` and returns the new value.
|
||||
*/
|
||||
export const setAndGet: {
|
||||
<A, ER, EW, RR, RW>(value: A): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<A, ER | EW, RR | RW>
|
||||
<A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, value: A): Effect.Effect<A, ER | EW, RR | RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, value: A) =>
|
||||
self.modify<A, never, never>(() => Effect.succeed([value, value] as const)),
|
||||
)
|
||||
|
||||
/**
|
||||
* Applies a synchronous update the value of a `Lens` and returns the new value.
|
||||
*/
|
||||
export const updateAndGet: {
|
||||
<A, ER, EW, RR, RW>(f: (a: A) => A): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<A, ER | EW, RR | RW>
|
||||
<A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => A): Effect.Effect<A, ER | EW, RR | RW>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => A) =>
|
||||
self.modify<A, never, never>(a => {
|
||||
const next = f(a)
|
||||
return Effect.succeed([next, next] as const)
|
||||
}),
|
||||
)
|
||||
|
||||
/**
|
||||
* Applies an effectful update to the value of a `Lens` and returns the new value.
|
||||
*/
|
||||
export const updateAndGetEffect: {
|
||||
<A, ER, EW, RR, RW, E, R>(f: (a: A) => Effect.Effect<A, E, R>): (self: Lens<A, ER, EW, RR, RW>) => Effect.Effect<A, ER | EW | E, RR | RW | R>
|
||||
<A, ER, EW, RR, RW, E, R>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => Effect.Effect<A, E, R>): Effect.Effect<A, ER | EW | E, RR | RW | R>
|
||||
} = Function.dual(2, <A, ER, EW, RR, RW, E, R>(self: Lens<A, ER, EW, RR, RW>, f: (a: A) => Effect.Effect<A, E, R>) =>
|
||||
self.modify<A, E, R>(a => Effect.flatMap(
|
||||
f(a),
|
||||
next => Effect.succeed([next, next] as const),
|
||||
)),
|
||||
)
|
||||
67
packages/effect-fc/src/PropertyPath.test.ts
Normal file
67
packages/effect-fc/src/PropertyPath.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Option } from "effect"
|
||||
import * as PropertyPath from "./PropertyPath.js"
|
||||
|
||||
|
||||
describe("immutableSet with arrays", () => {
|
||||
test("sets a top-level array element", () => {
|
||||
const arr = [1, 2, 3]
|
||||
const result = PropertyPath.immutableSet(arr, [1], 99)
|
||||
expect(result).toEqual(Option.some([1, 99, 3]))
|
||||
})
|
||||
|
||||
test("does not mutate the original array", () => {
|
||||
const arr = [1, 2, 3]
|
||||
PropertyPath.immutableSet(arr, [0], 42)
|
||||
expect(arr).toEqual([1, 2, 3])
|
||||
})
|
||||
|
||||
test("sets the first element of an array", () => {
|
||||
const arr = ["a", "b", "c"]
|
||||
const result = PropertyPath.immutableSet(arr, [0], "z")
|
||||
expect(result).toEqual(Option.some(["z", "b", "c"]))
|
||||
})
|
||||
|
||||
test("sets the last element of an array", () => {
|
||||
const arr = [10, 20, 30]
|
||||
const result = PropertyPath.immutableSet(arr, [2], 99)
|
||||
expect(result).toEqual(Option.some([10, 20, 99]))
|
||||
})
|
||||
|
||||
test("sets a nested array element inside an object", () => {
|
||||
const obj = { tags: ["foo", "bar", "baz"] }
|
||||
const result = PropertyPath.immutableSet(obj, ["tags", 1], "qux")
|
||||
expect(result).toEqual(Option.some({ tags: ["foo", "qux", "baz"] }))
|
||||
})
|
||||
|
||||
test("sets a deeply nested value inside an array of objects", () => {
|
||||
const obj = { items: [{ name: "alice" }, { name: "bob" }] }
|
||||
const result = PropertyPath.immutableSet(obj, ["items", 0, "name"], "charlie")
|
||||
expect(result).toEqual(Option.some({ items: [{ name: "charlie" }, { name: "bob" }] }))
|
||||
})
|
||||
|
||||
test("sets a value in a nested array", () => {
|
||||
const matrix = [[1, 2], [3, 4]]
|
||||
const result = PropertyPath.immutableSet(matrix, [1, 0], 99)
|
||||
expect(result).toEqual(Option.some([[1, 2], [99, 4]]))
|
||||
})
|
||||
|
||||
test("returns Option.none() for an out-of-bounds index", () => {
|
||||
const arr = [1, 2, 3]
|
||||
const result = PropertyPath.immutableSet(arr, [5], 99)
|
||||
expect(result).toEqual(Option.none())
|
||||
})
|
||||
|
||||
test("returns Option.none() for a non-numeric key on an array", () => {
|
||||
const arr = [1, 2, 3]
|
||||
// @ts-expect-error intentionally wrong key type
|
||||
const result = PropertyPath.immutableSet(arr, ["length"], 0)
|
||||
expect(result).toEqual(Option.none())
|
||||
})
|
||||
|
||||
test("empty path returns Option.some of the value itself", () => {
|
||||
const arr = [1, 2, 3]
|
||||
const result = PropertyPath.immutableSet(arr, [], [9, 9, 9] as any)
|
||||
expect(result).toEqual(Option.some([9, 9, 9]))
|
||||
})
|
||||
})
|
||||
183
packages/effect-fc/src/SubscriptionSubRef.test.ts
Normal file
183
packages/effect-fc/src/SubscriptionSubRef.test.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Chunk, Effect, Ref, SubscriptionRef } from "effect"
|
||||
import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
||||
|
||||
|
||||
describe("SubscriptionSubRef with array refs", () => {
|
||||
test("creates a subref for a single array element using path", async () => {
|
||||
const value = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make([{ name: "alice" }, { name: "bob" }, { name: "charlie" }]),
|
||||
parent => {
|
||||
const subref = SubscriptionSubRef.makeFromPath(parent, [1, "name"])
|
||||
return subref.get
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(value).toBe("bob")
|
||||
})
|
||||
|
||||
test("modifies a single array element via subref", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make([{ name: "alice" }, { name: "bob" }, { name: "charlie" }]),
|
||||
parent => {
|
||||
const subref = SubscriptionSubRef.makeFromPath(parent, [1, "name"])
|
||||
return Effect.flatMap(
|
||||
Ref.set(subref, "bob-updated"),
|
||||
() => Ref.get(parent),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result).toEqual([{ name: "alice" }, { name: "bob-updated" }, { name: "charlie" }])
|
||||
})
|
||||
|
||||
test("modifies array element at index 0", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make([10, 20, 30]),
|
||||
parent => {
|
||||
const subref = SubscriptionSubRef.makeFromPath(parent, [0])
|
||||
return Effect.flatMap(
|
||||
Ref.set(subref, 99),
|
||||
() => Ref.get(parent),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result).toEqual([99, 20, 30])
|
||||
})
|
||||
|
||||
test("modifies array element at last index", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make(["a", "b", "c"]),
|
||||
parent => {
|
||||
const subref = SubscriptionSubRef.makeFromPath(parent, [2])
|
||||
return Effect.flatMap(
|
||||
Ref.set(subref, "z"),
|
||||
() => Ref.get(parent),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result).toEqual(["a", "b", "z"])
|
||||
})
|
||||
|
||||
test("modifies nested array element", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make([[1, 2], [3, 4], [5, 6]]),
|
||||
parent => {
|
||||
const subref = SubscriptionSubRef.makeFromPath(parent, [1, 0])
|
||||
return Effect.flatMap(
|
||||
Ref.set(subref, 99),
|
||||
() => Ref.get(parent),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result).toEqual([[1, 2], [99, 4], [5, 6]])
|
||||
})
|
||||
|
||||
test("uses modifyEffect to transform array element", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make([{ count: 1 }, { count: 2 }, { count: 3 }]),
|
||||
parent => {
|
||||
const subref = SubscriptionSubRef.makeFromPath(parent, [1, "count"])
|
||||
return Effect.flatMap(
|
||||
Ref.update(subref, count => count + 100),
|
||||
() => Effect.map(Ref.get(parent), parentValue => ({ result: 102, parentValue })),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result.result).toBe(102) // count + 100
|
||||
expect(result.parentValue).toEqual([{ count: 1 }, { count: 102 }, { count: 3 }]) // count + 100
|
||||
})
|
||||
|
||||
test("uses modify to transform array element", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make([10, 20, 30]),
|
||||
parent => {
|
||||
const subref = SubscriptionSubRef.makeFromPath(parent, [1])
|
||||
return Effect.flatMap(
|
||||
Ref.update(subref, x => x + 5),
|
||||
() => Effect.map(Ref.get(parent), parentValue => ({ result: 25, parentValue })),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result.result).toBe(25) // 20 + 5
|
||||
expect(result.parentValue).toEqual([10, 25, 30]) // 20 + 5
|
||||
})
|
||||
|
||||
test("makeFromChunkIndex modifies chunk element", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make(Chunk.make(100, 200, 300)),
|
||||
parent => {
|
||||
const subref = SubscriptionSubRef.makeFromChunkIndex(parent, 1)
|
||||
return Effect.flatMap(
|
||||
Ref.set(subref, 999),
|
||||
() => Ref.get(parent),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(Chunk.toReadonlyArray(result)).toEqual([100, 999, 300])
|
||||
})
|
||||
|
||||
test("makeFromGetSet with custom getter/setter for array element", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make([{ id: 1, value: "a" }, { id: 2, value: "b" }]),
|
||||
parent => {
|
||||
const subref = SubscriptionSubRef.makeFromGetSet(parent, {
|
||||
get: arr => arr[0].value,
|
||||
set: (arr, newValue) => [
|
||||
{ ...arr[0], value: newValue },
|
||||
...arr.slice(1),
|
||||
],
|
||||
})
|
||||
return Effect.flatMap(
|
||||
Ref.set(subref, "updated"),
|
||||
() => Ref.get(parent),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(result).toEqual([{ id: 1, value: "updated" }, { id: 2, value: "b" }])
|
||||
})
|
||||
|
||||
test("does not mutate original array when modifying via subref", async () => {
|
||||
const original = [{ name: "alice" }, { name: "bob" }]
|
||||
const result = await Effect.runPromise(
|
||||
Effect.flatMap(
|
||||
SubscriptionRef.make(original),
|
||||
parent => {
|
||||
const subref = SubscriptionSubRef.makeFromPath(parent, [0, "name"])
|
||||
return Effect.flatMap(
|
||||
Ref.set(subref, "alice-updated"),
|
||||
() => Ref.get(parent),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(original).toEqual([{ name: "alice" }, { name: "bob" }]) // original unchanged
|
||||
expect(result).toEqual([{ name: "alice-updated" }, { name: "bob" }]) // new value in ref
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,7 @@ export * as Async from "./Async.js"
|
||||
export * as Component from "./Component.js"
|
||||
export * as ErrorObserver from "./ErrorObserver.js"
|
||||
export * as Form from "./Form.js"
|
||||
export * as Lens from "./Lens.js"
|
||||
export * as Memoized from "./Memoized.js"
|
||||
export * as Mutation from "./Mutation.js"
|
||||
export * as PropertyPath from "./PropertyPath.js"
|
||||
|
||||
@@ -34,5 +34,6 @@
|
||||
]
|
||||
},
|
||||
|
||||
"include": ["./src"]
|
||||
"include": ["./src"],
|
||||
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform": "^0.94.2",
|
||||
"@effect/platform-browser": "^0.74.0",
|
||||
"@effect/platform": "^0.96.0",
|
||||
"@effect/platform-browser": "^0.76.0",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@typed/id": "^0.17.2",
|
||||
"effect": "^3.19.15",
|
||||
|
||||
Reference in New Issue
Block a user