diff --git a/packages/effect-lens/src/Lens.ts b/packages/effect-lens/src/Lens.ts index 1468d87..5ef8119 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, Readable, type Runtime, Stream, type SubscriptionRef, type SynchronizedRef } from "effect" +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 type { NoSuchElementException } from "effect/Cause" import * as Subscribable from "./Subscribable.js" @@ -17,80 +17,167 @@ export interface Lens { readonly [LensTypeId]: LensTypeId - readonly modify: ( + readonly modifyEffect: ( f: (a: A) => Effect.Effect ) => Effect.Effect } -/** - * Internal `Lens` implementation. - */ -export class LensImpl -extends Pipeable.Class() implements Lens { - readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId - readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId - readonly [LensTypeId]: LensTypeId = LensTypeId - - constructor( - readonly get: Effect.Effect, - readonly changes: Stream.Stream, - readonly modify: ( - f: (a: A) => Effect.Effect - ) => Effect.Effect, - ) { - super() - } -} - - /** * Checks whether a value is a `Lens`. */ export const isLens = (u: unknown): u is Lens => Predicate.hasProperty(u, LensTypeId) +export const LensWithInternalsTypeId: unique symbol = Symbol.for("@effect-fc/Lens/LensWithInternals") +export type LensWithInternalsTypeId = typeof LensWithInternalsTypeId + +export interface LensWithInternals +extends Lens { + readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId + + readonly update: (a: A) => Effect.Effect + readonly semaphore: Effect.Semaphore +} + +export const isLensWithInternals = (u: unknown): u is LensWithInternals => Predicate.hasProperty(u, LensWithInternalsTypeId) + + +export declare namespace LensImpl { + export interface Source { + readonly get: Effect.Effect, + readonly changes: Stream.Stream, + readonly update: (a: A) => Effect.Effect, + readonly semaphore: Effect.Semaphore, + } +} + +export class LensImpl +extends Pipeable.Class() implements LensWithInternals { + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId + readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId + readonly [LensTypeId]: LensTypeId = LensTypeId + readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId + + constructor( + readonly get: Effect.Effect, + readonly changes: Stream.Stream, + readonly update: (a: A) => Effect.Effect, + readonly semaphore: Effect.Semaphore, + ) { + super() + } + + modifyEffect( + f: (a: A) => Effect.Effect + ) { + return this.semaphore.withPermits(1)( + Effect.flatMap( + this.get, + a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.update(next), b), + )) + ) + } +} /** * 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 = ( - options: { - readonly get: Effect.Effect - readonly changes: Stream.Stream - } & ( - | { - readonly modify: ( - f: (a: A) => Effect.Effect - ) => Effect.Effect - } - | { readonly set: (a: A) => Effect.Effect } - ) -): Lens => new LensImpl( - options.get, - options.changes, - Predicate.hasProperty(options, "modify") - ? options.modify - : ( - f: (a: A) => Effect.Effect - ) => Effect.flatMap( - options.get, - a => Effect.flatMap(f(a), ([b, next]) => Effect.as(options.set(next), b) - )), -) + source: LensImpl.Source +): Lens => new LensImpl(source.get, source.changes, source.update, source.semaphore) + + +export class LensLazyImpl +extends Pipeable.Class() implements LensWithInternals { + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId + readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId + readonly [LensTypeId]: LensTypeId = LensTypeId + readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId + + constructor( + readonly source: LensImpl.Source + ) { + super() + } + + get get() { return this.source.get } + get changes() { return this.source.changes } + get update() { return this.source.update } + get semaphore() { return this.source.semaphore } + + modifyEffect( + f: (a: A) => Effect.Effect + ) { + return this.semaphore.withPermits(1)( + Effect.flatMap( + this.get, + a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.update(next), b), + )) + ) + } +} + +/** + * Creates a `Lens` by supplying how to read the current value, observe changes, and apply transformations. + */ +export const makeLazy = ( + source: LensImpl.Source +): Lens => new LensLazyImpl(source) + + +export declare namespace SubscriptionRefLensImpl { + export interface SubscriptionRefWithInternals + extends SubscriptionRef.SubscriptionRef { + readonly ref: Ref.Ref + readonly pubsub: PubSub.PubSub + readonly semaphore: Effect.Semaphore + } +} + +export class SubscriptionRefLensImpl +extends Pipeable.Class() implements LensWithInternals { + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId + readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId + readonly [LensTypeId]: LensTypeId = LensTypeId + readonly [LensWithInternalsTypeId]: LensWithInternalsTypeId = LensWithInternalsTypeId + + readonly ref: SubscriptionRefLensImpl.SubscriptionRefWithInternals + + constructor( + ref: SubscriptionRef.SubscriptionRef + ) { + super() + this.ref = ref as SubscriptionRefLensImpl.SubscriptionRefWithInternals + } + + get get() { return this.ref.get } + get changes() { return this.ref.changes } + update(a: A) { + return Effect.zipLeft( + Ref.set(this.ref.ref, a), + PubSub.publish(this.ref.pubsub, a), + ) + } + get semaphore() { return this.ref.semaphore } + + modifyEffect( + f: (a: A) => Effect.Effect + ) { + return this.semaphore.withPermits(1)( + Effect.flatMap( + this.get, + a => Effect.flatMap(f(a), ([b, next]) => Effect.as(this.update(next), b), + )) + ) + } +} /** * Creates a `Lens` that proxies a `SubscriptionRef`. */ export const fromSubscriptionRef = ( ref: SubscriptionRef.SubscriptionRef -): Lens => make({ - get get() { return ref.get }, - get changes() { return ref.changes }, - modify: ( - f: (a: A) => Effect.Effect - ) => ref.modifyEffect(f), -}) +): Lens => new SubscriptionRefLensImpl(ref) + /** * Creates a `Lens` that proxies a `SynchronizedRef`. @@ -108,6 +195,7 @@ export const fromSynchronizedRef = ( ) => ref.modifyEffect(f), }) + /** * Flattens an effectful `Lens`. */ @@ -116,11 +204,9 @@ export const unwrap = ( ): Lens => make({ get: Effect.flatMap(effect, l => l.get), changes: Stream.unwrap(Effect.map(effect, l => l.changes)), - modify: ( - f: (a: A) => Effect.Effect - ) => Effect.flatMap(effect, l => l.modify(f)), -}) + update: a => Effect.flatMap(effect, l => (l as LensWithInternals).update(a)) +}) /** * Derives a new `Lens` by applying synchronous getters and setters over the focused value.