Fix v4 docs
Lint / lint (push) Successful in 16s

This commit is contained in:
Julien Valverdé
2026-06-22 00:09:53 +02:00
parent e15802976d
commit 6f61e1e57d
+263 -110
View File
@@ -1,137 +1,290 @@
# Effect Lens Next
# Effect Lens
An effectful Lens for Effect v4.
`effect-lens-next` provides a bidirectional view into shared state. A Lens can read, observe, and atomically update a value while focusing on a nested part of a larger structure.
A Lens type for [Effect](https://effect.website/) to easily manage nested state.
## Install
```sh
npm install effect@4.0.0-beta.85 effect-lens-next
```
npm install effect-lens@beta effect@4.0.0-beta.85
yarn add effect-lens@beta effect@4.0.0-beta.85
bun add effect-lens@beta effect@4.0.0-beta.85
```
This release targets Effect `4.0.0-beta.85` exactly while Effect v4 is in beta.
## Peer dependencies
- `effect` 4.0.0-beta.85
## Quickstart
```ts
import { Effect, Stream, SubscriptionRef } from "effect"
import { Lens } from "effect-lens-next"
A Lens is an effectful abstraction for focusing on (i.e., getting, subscribing to, setting, or modifying) a specific part of a larger immutable data structure, without losing the surrounding context.
const program = Effect.gen(function*() {
const state = yield* SubscriptionRef.make({ count: 0, label: "Counter" })
Picture it as a proxy to a separate data source with a similar API to Effect's `SubscriptionRef`, that can point to
a nested part of a data structure (I.E.: a `SubscriptionRef` holds an array of numbers, a Lens can proxy that array
or the number at a specific index).
const count = state.pipe(
Lens.fromSubscriptionRef,
Lens.focusObjectOn("count")
)
yield* Lens.update(count, (value) => value + 1)
const current = yield* Lens.get(count)
yield* count.changes.pipe(
Stream.take(1),
Stream.runForEach((value) => Effect.log(`count: ${value}`))
)
return current
})
```
## Lens Type
```ts
Lens.Lens<
A, // focused value
ER, // read errors
EW, // write errors
RR, // read requirements
RW // write requirements
What makes a Lens effectful is the fact that the proxy logic uses effects, which means reading from or writing to a
Lens can fail or have requirements:
```typescript
Lens<
A, // Type of the value the lens is focused on
ER, // Errors that can happen when reading
EW, // Errors that can happen when writing
RR, // Requirements for reading
RW // Requirements for writing
>
```
## Constructors
- `Lens.fromSubscriptionRef` creates a reactive Lens.
- `Lens.fromSynchronizedRef` creates a non-reactive Lens whose stream emits its value when evaluated.
- `Lens.fromRef` creates a non-reactive Lens and returns it in an Effect because it allocates a semaphore.
- `Lens.make` creates a Lens from custom `get`, `changes`, `commit`, and `lock` operations.
- `Lens.unwrap` flattens an effect that produces a Lens.
### Creating a Lens
## Focusing
#### From an existing type
We provide a few helpers to create Lenses from some Effect types:
```typescript
// The ref is the data source
const ref = yield* SubscriptionRef.make([12, 87, 69])
```ts
import { Effect, SubscriptionRef } from "effect"
import { Lens } from "effect-lens-next"
// The lens acts as a proxy that allows reading, subscribing to and writing to that
// data source with a similar API to Effect's SubscriptionRef
const lens = Lens.fromSubscriptionRef(ref)
// ^ Lens.Lens<number[], never, never, never, never>
const program = Effect.gen(function*() {
const state = yield* SubscriptionRef.make({
users: [{ name: "Ada", score: 10 }]
})
const value = yield* Lens.get(lens)
yield* Effect.forkScoped(Stream.runForEach(lens.changes, Console.log))
yield* Lens.updateEffect(lens, values =>
Effect.fromOption(Array.replace(values, 1, 1664))
)
```
const score = state.pipe(
Currently available:
- `fromSubscriptionRef`
- `fromSynchronizedRef` (note: since `SynchronizedRef` is not reactive (does not produce a stream of value changes), the resulting Lens' `changes` stream will only emit the current value of the lens when evaluated, and nothing else)
- `fromRef` (returns an effect because it creates an internal lock)
#### Manually
You can also create Lenses manually using `make` by providing:
- `get`: an effect that reads the current value,
- `changes`: a stream of value changes,
- `commit`: an effectful write primitive,
- `lock`: an effect that produces the lock used to serialize writes.
You can get pretty creative! Here's an example of a Lens that points to a specific key of the browser `LocalStorage`:
```typescript
// \/ Lens<Option.Option<string>, PlatformError, PlatformError, never, never>
const lens = Effect.all([
KeyValueStore.KeyValueStore,
Effect.succeed("someKey"),
Semaphore.make(1),
]).pipe(
Effect.map(([kv, key, semaphore]) => Lens.make({
get: kv.get(key),
changes: kv.get(key).pipe(
Effect.map(Stream.make),
Effect.map(a => Stream.concat(
a,
BrowserStream.fromEventListenerWindow("storage").pipe(
Stream.filter(event => event.key === key),
Stream.map(event => Option.fromNullable(event.newValue)),
),
)),
Stream.unwrap,
),
commit: a => Option.isSome(a)
? kv.set(key, a.value)
: kv.remove(key),
lock: Effect.succeed(semaphore.withPermits(1)),
})),
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
Lens.unwrap,
)
```
Note: while Lens supports asynchronous effects for the proxy logic, we would recommend keeping them synchronous to preserve atomicity.
### Focusing
Lenses can focus on a nested part of the data type they point to.
What does this mean? Let's say you have a Lens with this signature:
```typescript
Lens<{ readonly a: string, readonly b: number }, never, never, never, never>
```
*Focusing this Lens on `a`* means deriving a new Lens that points to the `a` field of the struct the current Lens points to, resulting in a:
```typescript
Lens<string, never, never, never, never>
```
Focused Lenses work just the same as a Lens that points directly to a data source and can be read, subscribed to or written to.
Writing to them will properly update parent Lenses or data sources. Such updates can be performed in both a mutable or an immutable manner depending on your choice.
This is a very powerful pattern as it enables you to keep your state in some shared data store while allowing you to pass specific parts of that state to some parts of your application. Very useful for frontend development!
#### Using built-in transforms
We provide a few helpers to create focused Lenses:
```typescript
interface User {
readonly name: string
readonly age: DateTime.Utc
}
// The state of your app
const ref = yield* SubscriptionRef.make<{
readonly users: readonly User[]
}>({
users: [
{ name: "Jean Dupont", age: yield* Effect.fromOption(DateTime.make("03/25/1969")) },
{ name: "Juan Joya Borja", age: yield* Effect.fromOption(DateTime.make("04/05/1956")) },
{ name: "Benzemonstre", age: yield* Effect.fromOption(DateTime.make("06/12/2000")) },
]
})
// \/ Lens<User, NoSuchElementError, NoSuchElementError, never, never>
const jeanDupontLens = ref.pipe(
Lens.fromSubscriptionRef, // Creates a lens that proxies the ref
Lens.focusObjectOn("users"), // Creates a focused lens that points to the users field
Lens.focusArrayAt(0), // Creates a focused lens that points to the first entry of the user array
)
// Reading or writing from this lens can fail with NoSuchElementError
// This is because of Lens.focusArrayAt(0), as reading and writing to an array is an unsafe operation
const jeanDupont = yield* Lens.get(jeanDupontLens)
yield* Lens.set(
// You can focus even further down
Lens.focusObjectOn(jeanDupontLens, "age"),
yield* Effect.fromOption(DateTime.make("03/25/1970")),
)
// Mutations with the parent state are performed immutably by default
// unless you use a specific mutable transform such as 'focusObjectOnWritable'
```
Currently available:
| Name | Description | Parent state mutation behavior | Notes |
| - | - | - | - |
| `focusObjectOn` | Focuses to a field of an object. Replaces the parent object immutably when writing to the focused field | Immutable | |
| `focusObjectOnWritable` | Focuses to a writable field of an object. Mutates the parent object in place via the writable field | Mutable | Type-safe: will not allow you to mutate `readonly` fields |
| `focusArrayAt` | Focuses to an indexed entry of an array. Replaces the parent array immutably when writing to the focused index | Immutable | |
| `focusMutableArrayAt` | Focuses to an indexed entry of an array. Mutates the parent array in place at the focused index | Mutable | Type-safe: will not allow you to mutate `readonly` arrays |
| `focusTupleAt` | Focuses to an indexed entry of a readonly tuple. Replaces the parent tuple immutably when writing to the focused index | Immutable | |
| `focusMutableTupleAt` | Focuses to an indexed entry of a mutable tuple. Mutates the parent tuple in place at the focused index | Mutable | Type-safe: will not allow you to mutate `readonly` tuples |
| `focusChunkAt` | Focuses to an indexed entry of a `Chunk`. Replaces the parent `Chunk` immutably when writing to the focused element | Immutable | |
| `focusOption` | Focuses to the value inside an `Option`. Wraps writes back into `Option.some` | Immutable | Reading or writing fails with `NoSuchElementError` when the parent option is `None` |
#### Manually
You can create focused Lenses by composing them manually using `map`, `mapEffect` and `unwrap`:
```typescript
const ref = yield* SubscriptionRef.make<readonly User[]>([
{ name: "Jean Dupont", age: yield* Effect.fromOption(DateTime.make("03/25/1969")) },
{ name: "Juan Joya Borja", age: yield* Effect.fromOption(DateTime.make("04/05/1956")) },
{ name: "Benzemonstre", age: yield* Effect.fromOption(DateTime.make("06/12/2000")) },
])
// \/ Lens<User, NoSuchElementError, NoSuchElementError, never, never>
const benzemonstreLens = ref.pipe(
Lens.fromSubscriptionRef,
// Manually focus
Lens.mapEffect(
// Getter:
a => Effect.fromOption(Array.get(a, 2)),
// Setter:
(a, b) => Effect.fromOption(Array.replace(a, 2, b)),
// ^ The current Lens value (readonly User[])
// ^ The new focused value to push (User)
),
)
// Both Array.get and Array.replace return an Option, which Effect.fromOption
// converts into Effect<A, NoSuchElementError>
// As you can see, this is automatically tracked by the Lens type
```
#### Low-level derived lenses
For advanced cases, you can derive a Lens manually using `derive`. This is the primitive used by the built-in transforms.
A derived Lens describes how to transform three parent channels:
- `resolve`: reads the parent and returns the focused value plus a `commit` function to rebuild the parent,
- `mapStream`: transforms the parent `changes` stream,
- `mapLock`: transforms the parent write lock.
Most custom focusing logic should use `map` or `mapEffect`, but `derive` is useful when you need full control over read, stream, lock, and write-back behavior.
```typescript
declare const lens: Lens.Lens<User, never, never, never, never>
const nameLens = lens.pipe(
Lens.derive({
resolve: parent => Effect.map(
parent,
resolved => ({
value: resolved.value.name,
commit: next => resolved.commit(
Effect.map(next, name => ({
...resolved.value,
name,
})),
),
}),
),
mapStream: Stream.map(user => user.name),
// This derived Lens does not add lock behavior, so it reuses the parent lock.
mapLock: identity,
}),
)
```
### Subscribable
Effect 4 no longer provides `Subscribable` and `Readable`. This library provides its own `Subscribable` interface, which you can use as a constraint to allow some parts of your app to only read and subscribe to the Lenses you provide them:
```typescript
const ref = yield* SubscriptionRef.make<{
readonly users: readonly User[]
}>({ users: [...] })
const someFunctionThatShouldOnlyHaveReadonlyAccessToTheState = (
usersSub: Subscribable.Subscribable<readonly User[], never, never>
) => Effect.gen(function*() {
// Do whatever
const usersCountSub = Subscribable.map(usersSub, a => a.length)
const users = yield* usersSub.get
yield* Effect.forkScoped(Stream.runForEach(usersSub.changes, ...))
})
const lens = ref.pipe(
Lens.fromSubscriptionRef,
Lens.focusObjectOn("users"),
Lens.focusArrayAt(0),
Lens.focusObjectOn("score")
)
yield* Lens.set(score, 11)
})
)
yield* someFunctionThatShouldOnlyHaveReadonlyAccessToTheState(lens)
```
Available focus operations include object fields, mutable object fields, arrays, mutable arrays, tuples, mutable tuples, `Chunk` values, and `Option` values. Safe indexed operations fail with `Cause.NoSuchElementError` when the index is absent.
#### Focusing
This library provides a `Subscribable` module with value and error transforms, recovery (`catch`, `catchCause`, `orElse`, `orElseSucceed`, and `retry`), and transforms to narrow the focus of `Subscribable`'s, same as Lenses:
```typescript
import { Subscribable } from "effect-lens"
Effect v4 no longer treats `Option` as an Effect. Custom effectful mappings should convert optional values explicitly:
declare const sub: Subscribable.Subscribable<readonly { name: string }[], never, never>
```ts
import { Array, Effect } from "effect"
Lens.mapEffect(
(values: ReadonlyArray<number>) => Effect.fromOption(Array.get(values, 0)),
(values, value) => Effect.fromOption(Array.replace(values, 0, value))
// \/ Subscribable.Subscribable<string, NoSuchElementError, never>
const nameSub = sub.pipe(
Subscribable.focusArrayAt(1),
Subscribable.focusObjectOn("name"),
)
```
## Updating
The update API follows Effect v4's `SubscriptionRef` conventions:
- `get`, `set`, `getAndSet`, `setAndGet`
- `modify`, `modifyEffect`
- `update`, `updateEffect`
- `getAndUpdate`, `getAndUpdateEffect`
- `updateAndGet`, `updateAndGetEffect`
- `modifySome`, `modifySomeEffect`
- `getAndUpdateSome`, `getAndUpdateSomeEffect`
- `updateSome`, `updateSomeEffect`
- `updateSomeAndGet`, `updateSomeAndGetEffect`
Conditional updates use `Option.none()` to skip both the write and change notification:
```ts
import { Effect, Option } from "effect"
yield* Lens.updateSome(lens, (value) =>
value < 10 ? Option.some(value + 1) : Option.none()
)
yield* Lens.updateSomeEffect(lens, (value) =>
Effect.succeed(value < 10 ? Option.some(value + 1) : Option.none())
)
```
## Subscribable
Effect v4 removed its `Readable` and `Subscribable` modules. This package provides a small compatible abstraction with `get` and `changes` properties:
```ts
import { Lens, Subscribable } from "effect-lens-next"
const users = Lens.fromSubscriptionRef(usersRef)
const count = Subscribable.focusArrayLength(users)
const current = yield* count.get
```
The module includes value transforms, error recovery (`catch`, `catchCause`, `orElse`, `orElseSucceed`, and `retry`), error inspection, `unwrap`, and the same readonly focus helpers used by Lens. A `SubscriptionRef` can first be converted with `Lens.fromSubscriptionRef`, since every Lens is already a Subscribable.
Currently available:
| Name | Description |
| - | - |
| `focusObjectOn` | Focuses to the field of an object |
| `focusArrayAt` | Focuses to an indexed entry of an array |
| `focusArrayLength` | Focuses to the length of an array |
| `focusTupleAt` | Focuses to an indexed entry of a tuple |
| `focusChunkAt` | Focuses to an indexed entry of a `Chunk` |
| `focusChunkSize` | Focuses to the size of a `Chunk` |
| `focusIterableSize` | Focuses to the size of an iterable |