From 5c1fdbad431efcd96154415d0aa4e68b43854344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 26 May 2026 04:18:59 +0200 Subject: [PATCH] Refactor --- packages/effect-lens/src/Lens.ts | 395 ++++++++++++++----------------- 1 file changed, 174 insertions(+), 221 deletions(-) diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index ecb2adf..f467d71 100644 --- a/packages/effect-lens/src/Lens.ts +++ b/packages/effect-lens/src/Lens.ts @@ -1,4 +1,4 @@ -import { Array, Chunk, type Context, Effect, Function, identity, type ManagedRuntime, Option, Pipeable, Predicate, PubSub, Readable, Ref, type Runtime, Stream, type SubscriptionRef, type SynchronizedRef } from "effect" +import { Array, Chunk, type Context, Effect, Function, identity, Option, Pipeable, Predicate, PubSub, Readable, Ref, Stream, type SubscriptionRef, type SynchronizedRef } from "effect" import type { NoSuchElementException } from "effect/Cause" import * as Subscribable from "./Subscribable.js" @@ -28,60 +28,24 @@ extends Subscribable.Subscribable { export const isLens = (u: unknown): u is Lens => Predicate.hasProperty(u, LensTypeId) -export const LensStepTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensStep") -export type LensStepTypeId = typeof LensStepTypeId - -export interface LensFrame { - readonly value: A - readonly commit: ( - next: Effect.Effect - ) => Effect.Effect -} - -export interface LensStep< - in out A, - in out B, - in out ER = never, - in out ESR = never, - in out EW = never, - in out ESW = never, - in out RR = never, - in out RSR = never, - in out RW = never, - in out RSW = never, -> { - readonly [LensStepTypeId]: LensStepTypeId - readonly access: (effect: Effect.Effect, ESR, RSR>) => Effect.Effect, ER, RR> - readonly transformStream: (stream: Stream.Stream) => Stream.Stream -} - -export const isLensStep = (u: unknown): u is LensStep => Predicate.hasProperty(u, LensStepTypeId) - - export const LensImplTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensImpl") export type LensImplTypeId = typeof LensImplTypeId -export const isLensImpl = (u: unknown): u is LensImpl => Predicate.hasProperty(u, LensImplTypeId) - -export const asLensImpl = ( - lens: Lens -): LensImpl => { - if (!isLensImpl(lens)) - throw new Error("Not a 'LensImpl'.") - return lens as LensImpl +export declare namespace LensImpl { + export interface Frame { + readonly value: A + readonly commit: ( + next: Effect.Effect + ) => Effect.Effect + } } export abstract class LensImpl< in out A, - in out B, in out ER = never, - in out ESR = never, in out EW = never, - in out ESW = never, in out RR = never, - in out RSR = never, in out RW = never, - in out RSW = never, > extends Pipeable.Class() implements Lens { readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId @@ -89,39 +53,14 @@ extends Pipeable.Class() implements Lens { readonly [LensTypeId]: LensTypeId = LensTypeId readonly [LensImplTypeId]: LensImplTypeId = LensImplTypeId - readonly steps: readonly LensStep[] = [] - - abstract readonly sourceGet: Effect.Effect - abstract readonly sourceChanges: Stream.Stream - abstract sourceCommit(b: B): Effect.Effect + abstract readonly access: Effect.Effect, ER, RR> + abstract readonly changes: Stream.Stream abstract readonly withLock: (self: Effect.Effect) => Effect.Effect - get access(): Effect.Effect, ER, RR> { - let effect: Effect.Effect, unknown, unknown> = Effect.map( - this.sourceGet, - value => ({ - value, - commit: next => Effect.flatMap(next, value => this.sourceCommit(value as B)), - }), - ) - - for (const step of this.steps) - effect = step.access(effect) as Effect.Effect, unknown, unknown> - - return effect as Effect.Effect, ER, RR> - } - get get(): Effect.Effect { return Effect.map(this.access, frame => frame.value) } - get changes(): Stream.Stream { - let stream: Stream.Stream = this.sourceChanges - for (const step of this.steps) - stream = step.transformStream(stream) - return stream as Stream.Stream - } - modifyEffect( f: (a: A) => Effect.Effect, ): Effect.Effect { @@ -135,77 +74,128 @@ extends Pipeable.Class() implements Lens { } } +export const isLensImpl = (u: unknown): u is LensImpl => Predicate.hasProperty(u, LensImplTypeId) + +export const asLensImpl = ( + lens: Lens +): LensImpl => { + if (!isLensImpl(lens)) + throw new Error("Not a 'LensImpl'.") + return lens as LensImpl +} + export declare namespace LensLazyImpl { export interface Source { - readonly sourceGet: Effect.Effect - readonly sourceChanges: Stream.Stream - readonly sourceCommit: (b: B) => Effect.Effect + readonly get: Effect.Effect + readonly changes: Stream.Stream + readonly commit: (b: B) => Effect.Effect readonly withLock: (self: Effect.Effect) => Effect.Effect } } export class LensLazyImpl< - in out A, in out B, - in out ER = never, - in out ESR = never, - in out EW = never, in out ESW = never, - in out RR = never, + in out ESR = never, in out RSR = never, - in out RW = never, in out RSW = never, > -extends LensImpl { +extends LensImpl { constructor( readonly source: LensLazyImpl.Source, ) { super() } - get sourceGet() { return this.source.sourceGet } - get sourceChanges() { return this.source.sourceChanges } - sourceCommit(b: B) { return this.source.sourceCommit(b) } + get access(): Effect.Effect, ESR, RSR> { + return Effect.map( + this.source.get, + value => ({ + value, + commit: next => Effect.flatMap(next, value => this.source.commit(value)), + }), + ) + } + + get changes() { return this.source.changes } get withLock() { return this.source.withLock } } /** * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. */ -export const makeLazy = ( +export const make = ( source: LensLazyImpl.Source ): Lens => new LensLazyImpl(source) + +export declare namespace DerivedLensImpl { + export interface Source< + in out A, + in out B, + in out ER = never, + in out ESR = never, + in out EW = never, + in out ESW = never, + in out RR = never, + in out RSR = never, + in out RW = never, + in out RSW = never, + > { + readonly access: (effect: Effect.Effect, ESR, RSR>) => Effect.Effect, ER, RR> + readonly transformStream: (stream: Stream.Stream) => Stream.Stream + } +} + +export class DerivedLensImpl< + in out A, + in out B, + in out ER = never, + in out PER = never, + in out EW = never, + in out PEW = never, + in out RR = never, + in out PRR = never, + in out RW = never, + in out PRW = never, +> +extends LensImpl { + constructor( + readonly parent: LensImpl, + readonly source: DerivedLensImpl.Source, + ) { + super() + } + + get access(): Effect.Effect, ER, RR> { + return this.source.access(this.parent.access) + } + + get changes(): Stream.Stream { + return this.source.transformStream(this.parent.changes) + } + + get withLock() { + return this.parent.withLock + } +} + /** - * Derives a new `Lens` by immutably appending a step to an existing `LensImpl`. + * Derives a new `Lens` by linking a step to an existing parent lens. */ export const derive: { - ( - self: LensImpl, - step: LensStep, + ( + self: Lens, + source: DerivedLensImpl.Source, ): Lens ( - step: LensStep, - ): ( - self: LensImpl - ) => Lens -} = Function.dual(2, ( - self: LensImpl, - step: LensStep, -): Lens => Object.defineProperty( - Object.defineProperties( - Object.create(Object.getPrototypeOf(self)), - Object.getOwnPropertyDescriptors(self), - ), - "steps", - { - configurable: true, - enumerable: true, - value: [...self.steps, step as LensStep], - writable: false, - }, -) as Lens) + source: DerivedLensImpl.Source, + ): (self: Lens) => Lens +} = Function.dual(2, ( + self: Lens, + source: DerivedLensImpl.Source, +): Lens => new DerivedLensImpl(asLensImpl(self), source)) export declare namespace SynchronizedRefLensImpl { @@ -217,7 +207,7 @@ export declare namespace SynchronizedRefLensImpl { } export class SynchronizedRefLensImpl -extends LensImpl { +extends LensImpl { readonly ref: SynchronizedRefLensImpl.SynchronizedRefWithInternals constructor( @@ -227,9 +217,17 @@ extends LensImpl { this.ref = ref as SynchronizedRefLensImpl.SynchronizedRefWithInternals } - get sourceGet() { return this.ref.get } - get sourceChanges() { return Stream.unwrap(Effect.map(this.ref.get, Stream.make)) } - sourceCommit(a: A) { return Ref.set(this.ref.ref, a) } + get access(): Effect.Effect, never, never> { + return Effect.map( + this.ref.get, + value => ({ + value, + commit: next => Effect.flatMap(next, value => Ref.set(this.ref.ref, value)), + }), + ) + } + + get changes() { return Stream.unwrap(Effect.map(this.ref.get, Stream.make)) } get withLock() { return this.ref.withLock } } @@ -254,7 +252,7 @@ export declare namespace SubscriptionRefLensImpl { } export class SubscriptionRefLensImpl -extends LensImpl { +extends LensImpl { readonly ref: SubscriptionRefLensImpl.SubscriptionRefWithInternals constructor( @@ -264,9 +262,18 @@ extends LensImpl { this.ref = ref as SubscriptionRefLensImpl.SubscriptionRefWithInternals } - get sourceGet() { return this.ref.get } - get sourceChanges() { return this.ref.changes } - sourceCommit(a: A) { + get access(): Effect.Effect, never, never> { + return Effect.map( + this.ref.get, + value => ({ + value, + commit: next => Effect.flatMap(next, value => this.commit(value)), + }), + ) + } + + get changes() { return this.ref.changes } + commit(a: A) { return Effect.zipLeft( Ref.set(this.ref.ref, a), PubSub.publish(this.ref.pubsub, a), @@ -288,10 +295,10 @@ export const fromSubscriptionRef = ( */ export const unwrap = ( effect: Effect.Effect, E1, R1> -): Lens => makeLazy({ - sourceGet: Effect.flatMap(effect, l => l.get), - sourceChanges: Stream.unwrap(Effect.map(effect, l => l.changes)), - sourceCommit: a => Effect.flatMap( +): Lens => make({ + get: Effect.flatMap(effect, l => l.get), + changes: Stream.unwrap(Effect.map(effect, l => l.changes)), + commit: a => Effect.flatMap( effect, l => Effect.flatMap(asLensImpl(l).access, frame => frame.commit(Effect.succeed(a))), ), @@ -338,25 +345,19 @@ export const mapEffect: { self: Lens, get: (a: NoInfer) => Effect.Effect, set: (a: NoInfer, b: B) => Effect.Effect, ESet, RSet>, -): Lens => { - return derive( - asLensImpl(self), - { - [LensStepTypeId]: LensStepTypeId, - access: parent => Effect.flatMap( - parent, - frame => Effect.map( - get(frame.value), - value => ({ - value, - commit: next => frame.commit(Effect.flatMap(next, b => set(frame.value, b))), - }), - ), - ), - transformStream: stream => Stream.mapEffect(stream, get), - }, - ) -}) +): Lens => derive(self, { + access: parent => Effect.flatMap( + parent, + frame => Effect.map( + get(frame.value), + value => ({ + value, + commit: next => frame.commit(Effect.flatMap(next, b => set(frame.value, b))), + }), + ), + ), + transformStream: Stream.mapEffect(get), +})) /** * Derives a new `Lens` by applying synchronous getters and setters over the value inside an `Option`. @@ -438,11 +439,10 @@ export const mapStream: { } = Function.dual(2, ( self: Lens, f: (changes: Stream.Stream, NoInfer, NoInfer>) => Stream.Stream, NoInfer, NoInfer>, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, - access: parent => parent, +): Lens => derive(self, { + access: identity, transformStream: f, -} as LensStep)) +})) /** @@ -461,11 +461,10 @@ export const mapErrorRead: { } = Function.dual(2, ( self: Lens, f: (error: NoInfer) => E2, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, - access: parent => Effect.mapError(parent, f), - transformStream: stream => Stream.mapError(stream, f), -} as LensStep)) +): Lens => derive(self, { + access: Effect.mapError(f), + transformStream: Stream.mapError(f), +})) /** * Transforms modify errors of a `Lens`. @@ -484,14 +483,13 @@ export const mapErrorWrite: { } = Function.dual(2, ( self: Lens, f: (error: unknown) => E2, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, +): Lens => derive(self, { access: parent => Effect.map(parent, frame => ({ value: frame.value, commit: next => Effect.mapError(frame.commit(next), f), })), - transformStream: stream => stream, -} as LensStep)) + transformStream: identity, +})) /** * Transforms all errors of a `Lens`. @@ -510,8 +508,7 @@ export const mapError: { } = Function.dual(2, ( self: Lens, f: (error: unknown) => E2, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, +): Lens => derive(self, { access: parent => Effect.map( Effect.mapError(parent, f), frame => ({ @@ -519,8 +516,8 @@ export const mapError: { commit: next => Effect.mapError(frame.commit(next), f), }), ), - transformStream: stream => Stream.mapError(stream, f), -} as LensStep)) + transformStream: Stream.mapError(f), +})) /** * Recovers from read failures of a `Lens`. @@ -538,11 +535,10 @@ export const catchAllRead: { } = Function.dual(2, ( self: Lens, f: (error: NoInfer) => Subscribable.Subscribable, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, - access: parent => Effect.catchAll(parent, error => asLensImpl(f(error) as Lens).access), - transformStream: stream => Stream.catchAll(stream, error => f(error).changes), -} as LensStep)) +): Lens => derive(self, { + access: Effect.catchAll(error => asLensImpl(f(error) as Lens).access), + transformStream: Stream.catchAll(error => f(error).changes), +})) /** * Runs an effect when read failures occur. @@ -560,11 +556,10 @@ export const tapErrorRead: { } = Function.dual(2, ( self: Lens, f: (error: NoInfer) => Effect.Effect, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, - access: parent => Effect.tapError(parent, f), - transformStream: stream => Stream.tapError(stream, f), -} as LensStep)) +): Lens => derive(self, { + access: Effect.tapError(f), + transformStream: Stream.tapError(f), +})) /** * Runs an effect when modify failures occur. @@ -583,14 +578,13 @@ export const tapErrorWrite: { } = Function.dual(2, ( self: Lens, f: (error: unknown) => Effect.Effect, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, +): Lens => derive(self, { access: parent => Effect.map(parent, frame => ({ value: frame.value, commit: next => Effect.tapError(frame.commit(next), f), })), - transformStream: stream => stream, -} as LensStep)) + transformStream: identity, +})) /** * Runs an effect when any `Lens` failure occurs. @@ -609,8 +603,7 @@ export const tapError: { } = Function.dual(2, ( self: Lens, f: (error: unknown) => Effect.Effect, -): Lens => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, +): Lens => derive(self, { access: parent => Effect.map( Effect.tapError(parent, f), frame => ({ @@ -618,8 +611,8 @@ export const tapError: { commit: next => Effect.tapError(frame.commit(next), f), }), ), - transformStream: stream => Stream.tapError(stream, f), -} as LensStep)) + transformStream: Stream.tapError(f), +})) /** @@ -636,8 +629,7 @@ export const provideContext: { } = Function.dual(2, ( self: Lens, context: Context.Context, -): Lens, Exclude> => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, +): Lens, Exclude> => derive(self, { access: parent => Effect.map( Effect.provide(parent, context), frame => ({ @@ -645,46 +637,8 @@ export const provideContext: { commit: next => Effect.provide(frame.commit(Effect.provide(next, context)), context), }), ), - transformStream: stream => Stream.provideSomeContext(stream, context), -} as LensStep, RR, Exclude, RW>)) - -/** - * Provides a `Runtime` or `ManagedRuntime` to a `Lens`, removing it from both the read and write environments. - * - * `ManagedRuntime` may add its construction errors to both the read and write error channels. - */ -export const provideRuntime: { - ( - runtime: Runtime.Runtime, - ): (self: Lens) => Lens, Exclude> - ( - managedRuntime: ManagedRuntime.ManagedRuntime, - ): (self: Lens) => Lens, Exclude> - ( - self: Lens, - runtime: Runtime.Runtime, - ): Lens, Exclude> - ( - self: Lens, - runtime: ManagedRuntime.ManagedRuntime, - ): Lens, Exclude> -} = Function.dual(2, ( - self: Lens, - runtime: Runtime.Runtime, -) => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, - access: parent => Effect.map( - Effect.provide(parent, runtime), - frame => ({ - value: frame.value, - commit: next => Effect.provide(frame.commit(Effect.provide(next, runtime)), runtime), - }), - ), - transformStream: stream => Stream.unwrap(Effect.map( - Effect.provide(Effect.context(), runtime), - context => Stream.provideContext(stream, context), - )), -} as LensStep, RR, Exclude, RW>)) + transformStream: Stream.provideSomeContext(context), +})) /** * Provides a single service to a `Lens`, removing it from both the read and write environments. @@ -706,8 +660,7 @@ export const provideService: { self: Lens, tag: Context.Tag, service: NoInfer, -): Lens, Exclude> => derive(asLensImpl(self), { - [LensStepTypeId]: LensStepTypeId, +): Lens, Exclude> => derive(self, { access: parent => Effect.map( Effect.provideService(parent, tag, service), frame => ({ @@ -715,8 +668,8 @@ export const provideService: { commit: next => Effect.provideService(frame.commit(Effect.provideService(next, tag, service)), tag, service), }), ), - transformStream: stream => Stream.provideService(stream, tag, service), -} as LensStep, RR, Exclude, RW>)) + transformStream: Stream.provideService(tag, service), +})) /**