0.1.3 #2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "effect-lens",
|
"name": "effect-lens",
|
||||||
"description": "An effectful Lens type to easily manage nested state",
|
"description": "An effectful Lens type to easily manage nested state",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./README.md",
|
"./README.md",
|
||||||
|
|||||||
@@ -1,9 +1,59 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { Chunk, Effect, SubscriptionRef } from "effect"
|
import { Chunk, Effect, Option, SubscriptionRef } from "effect"
|
||||||
import * as Lens from "./Lens.js"
|
import * as Lens from "./Lens.js"
|
||||||
|
|
||||||
|
|
||||||
describe("Lens", () => {
|
describe("Lens", () => {
|
||||||
|
test("mapOption transforms Some values and preserves None", async () => {
|
||||||
|
const result = await Effect.runPromise(
|
||||||
|
Effect.flatMap(
|
||||||
|
SubscriptionRef.make<Option.Option<number>>(Option.some(42)),
|
||||||
|
parent => {
|
||||||
|
const lens = Lens.mapOption(
|
||||||
|
Lens.fromSubscriptionRef(parent),
|
||||||
|
n => n * 2,
|
||||||
|
(_n, doubled) => doubled / 2,
|
||||||
|
)
|
||||||
|
return Effect.flatMap(
|
||||||
|
Lens.get(lens),
|
||||||
|
value => Effect.flatMap(
|
||||||
|
Lens.set(lens, Option.some(100)),
|
||||||
|
() => Effect.map(parent.get, parentValue => [value, parentValue] as const),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result[0]).toEqual(Option.some(84)) // 42 * 2
|
||||||
|
expect(result[1]).toEqual(Option.some(50)) // 100 / 2
|
||||||
|
})
|
||||||
|
|
||||||
|
test("mapOptionEffect transforms Some values with effects", async () => {
|
||||||
|
const result = await Effect.runPromise(
|
||||||
|
Effect.flatMap(
|
||||||
|
SubscriptionRef.make<Option.Option<number>>(Option.some(42)),
|
||||||
|
parent => {
|
||||||
|
const lens = Lens.mapOptionEffect(
|
||||||
|
Lens.fromSubscriptionRef(parent),
|
||||||
|
n => Effect.succeed(n * 2),
|
||||||
|
(_n, doubled) => Effect.succeed(doubled / 2),
|
||||||
|
)
|
||||||
|
return Effect.flatMap(
|
||||||
|
Lens.get(lens),
|
||||||
|
value => Effect.flatMap(
|
||||||
|
Lens.set(lens, Option.some(100)),
|
||||||
|
() => Effect.map(parent.get, parentValue => [value, parentValue] as const),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result[0]).toEqual(Option.some(84)) // 42 * 2
|
||||||
|
expect(result[1]).toEqual(Option.some(50)) // 100 / 2
|
||||||
|
})
|
||||||
|
|
||||||
test("focusObjectField 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(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Array, Chunk, Effect, Function, Pipeable, Predicate, Readable, Stream, type SubscriptionRef, type SynchronizedRef } from "effect"
|
import { Array, Chunk, Effect, Function, Option, Pipeable, Predicate, Readable, Stream, type SubscriptionRef, type SynchronizedRef } from "effect"
|
||||||
import type { NoSuchElementException } from "effect/Cause"
|
import type { NoSuchElementException } from "effect/Cause"
|
||||||
import * as Subscribable from "./Subscribable.js"
|
import * as Subscribable from "./Subscribable.js"
|
||||||
|
|
||||||
@@ -183,6 +183,72 @@ export const mapEffect: {
|
|||||||
)),
|
)),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives a new `Lens` by applying synchronous getters and setters over the value inside an `Option`.
|
||||||
|
*
|
||||||
|
* Similar to `Option.map`, this preserves the `Option` structure:
|
||||||
|
* - If the `Option` is `Some(a)`, applies the getter and setter to the inner value
|
||||||
|
* - If the `Option` is `None`, it remains `None`
|
||||||
|
*/
|
||||||
|
export const mapOption: {
|
||||||
|
<A, ER, EW, RR, RW, B>(
|
||||||
|
self: Lens<Option.Option<A>, ER, EW, RR, RW>,
|
||||||
|
get: (a: NoInfer<A>) => B,
|
||||||
|
set: (a: NoInfer<A>, b: B) => NoInfer<A>,
|
||||||
|
): Lens<Option.Option<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<Option.Option<A>, ER, EW, RR, RW>) => Lens<Option.Option<B>, ER, EW, RR, RW>
|
||||||
|
} = Function.dual(3, <A, ER, EW, RR, RW, B>(
|
||||||
|
self: Lens<Option.Option<A>, ER, EW, RR, RW>,
|
||||||
|
get: (a: NoInfer<A>) => B,
|
||||||
|
set: (a: NoInfer<A>, b: B) => NoInfer<A>,
|
||||||
|
): Lens<Option.Option<B>, ER, EW, RR, RW> => map(
|
||||||
|
self,
|
||||||
|
Option.map(get),
|
||||||
|
(opt, newOpt) => Option.match(opt, {
|
||||||
|
onSome: a => Option.map(newOpt, b => set(a, b)),
|
||||||
|
onNone: () => Option.none(),
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives a new `Lens` by applying effectful getters and setters over the value inside an `Option`.
|
||||||
|
*
|
||||||
|
* Similar to `Option.map`, this preserves the `Option` structure:
|
||||||
|
* - If the `Option` is `Some(a)`, applies the effectful getter and setter to the inner value
|
||||||
|
* - If the `Option` is `None`, it remains `None`
|
||||||
|
*/
|
||||||
|
export const mapOptionEffect: {
|
||||||
|
<A, ER, EW, RR, RW, B, EGet = never, RGet = never, ESet = never, RSet = never>(
|
||||||
|
self: Lens<Option.Option<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<Option.Option<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<Option.Option<A>, ER, EW, RR, RW>) => Lens<Option.Option<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<Option.Option<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<Option.Option<B>, ER | EGet, EW | ESet, RR | RGet, RW | RSet> => mapEffect(
|
||||||
|
self,
|
||||||
|
Option.match({
|
||||||
|
onSome: a => Effect.map(get(a), Option.some),
|
||||||
|
onNone: () => Effect.succeed(Option.none()),
|
||||||
|
}),
|
||||||
|
(opt, newOpt) => Option.match(opt, {
|
||||||
|
onSome: a => Option.match(newOpt, {
|
||||||
|
onSome: b => Effect.map(set(a, b), Option.some),
|
||||||
|
onNone: () => Effect.succeed(Option.none()),
|
||||||
|
}),
|
||||||
|
onNone: () => Effect.succeed(Option.none()),
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows transforming only the `changes` stream of a `Lens` while keeping the focus type intact.
|
* Allows transforming only the `changes` stream of a `Lens` while keeping the focus type intact.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user