@fuiste/optics-effect
v0.1.0
Published
Effect bindings for @fuiste/optics using Effect.Either
Downloads
7
Readme
@fuiste/optics-effect
Effect bindings for @fuiste/optics using effect's Either for safe get/set and composition.
Installation
pnpm add @fuiste/optics-effect
# peer deps
pnpm add effect -D
pnpm add @fuiste/opticsUsage
import { EffectLens, EffectPrism, EffectIso } from '@fuiste/optics-effect'
// Lens
type Person = { name: string; age: number }
const nameL = EffectLens<Person>().prop('name')
// Prism
type Address = { street: string; city: string }
type P = { name: string; address?: Address }
const addressP = EffectPrism<P>().of({ get: (p) => p.address, set: (a) => (p) => ({ ...p, address: a }) })
// Iso
const numStr = EffectIso<number, string>({ to: (n) => `${n}`, from: (s) => parseInt(s, 10) })See the test suite in test/effect.test.ts and @fuiste/optics README for more examples.
Optics in effect pipes
import { pipe } from 'effect'
import { EffectLens, EffectPrism, EffectIso } from '@fuiste/optics-effect'
// Sample domain
type Address = { street: string; city: string }
type Person = { name: string; address?: Address }
const nameL = EffectLens<Person>().prop('name')
const addressP = EffectPrism<Person>().of({ get: (p) => p.address, set: (a) => (p) => ({ ...p, address: a }) })
const cityL = EffectLens<Address>().prop('city')
const person: Person = { name: 'Ada', address: { street: '1', city: 'NYC' } }
// Lens get inside a pipe → Either<string, never>
const readName = pipe(person, nameL.get)
// Lens set inside a pipe → Either<Person, never>
const renamed = pipe(person, nameL.set('Lovelace'))
// Prism ∘ Lens compose; get inside a pipe → Either<string, EffectPrismNotFound>
const cityP = EffectPrism<Person>().compose(addressP, cityL)
const readCity = pipe(person, cityP.get)
// Iso to/from inside a pipe → Either<string, never> / Either<number, never>
const numStr = EffectIso<number, string>({ to: (n) => `${n}`, from: (s) => parseInt(s, 10) })
const asString = pipe(42, numStr.to)
const backToNum = pipe('42', numStr.from)These produce Either values. Lift to Effect when you need to sequence in the Effect monad, e.g. Effect.fromEither(readCity).
Optics in Effect.gen generators
import { Effect } from 'effect'
import { EffectLens, EffectPrism } from '@fuiste/optics-effect'
type Address = { street: string; city: string }
type Person = { name: string; address?: Address }
const nameL = EffectLens<Person>().prop('name')
const addressP = EffectPrism<Person>().of({ get: (p) => p.address, set: (a) => (p) => ({ ...p, address: a }) })
const program = Effect.gen(function* (_) {
const p: Person = { name: 'Ada', address: { street: '1', city: 'NYC' } }
// Get via lens (always Right)
const name = yield* _(nameL.get(p))
// Safely get via prism (fails with EffectPrismNotFound if missing)
const addr = yield* _(addressP.get(p))
// Update via prism then lens by composing first
const cityL = EffectLens<Address>().prop('city')
const cityP = EffectPrism<Person>().compose(addressP, cityL)
const updated = yield* _(cityP.set((c) => c.toUpperCase())(p))
return { name, addr, updated }
})Resolving optics returns within effects
EffectLens.get, EffectLens.set, EffectIso.to, and EffectIso.from produce Either.Right and never fail. EffectPrism.get may return Either.Left(EffectPrismNotFound), and EffectPrism.set may return Either.Left(EffectPrismNoOpSet) if the focus is missing.
import { pipe, Effect } from 'effect'
import { EffectPrism, EffectLens, EffectPrismNotFound, EffectPrismNoOpSet } from '@fuiste/optics-effect'
type Address = { street: string; city: string }
type Person = { name: string; address?: Address }
const addressP = EffectPrism<Person>().of({ get: (p) => p.address, set: (a) => (p) => ({ ...p, address: a }) })
const cityL = EffectLens<Address>().prop('city')
const cityP = EffectPrism<Person>().compose(addressP, cityL)
// Lift only when sequencing in Effect
const ensureCityLA = (p: Person) =>
pipe(
p,
cityP.set('LA'), // Either<Person, EffectPrismNoOpSet>
Effect.fromEither, // Effect<EffectPrismNoOpSet, Person>
Effect.catchAll((e) => // handle missing address
e instanceof EffectPrismNoOpSet
? Effect.succeed({ ...p, address: { street: '', city: 'LA' } })
: Effect.fail(e),
),
)
const readCity = (p: Person) =>
pipe(
p,
cityP.get, // Either<string, EffectPrismNotFound>
Effect.fromEither, // Effect<EffectPrismNotFound, string>
Effect.catchAll(() => Effect.succeed('Unknown')),
)Scripts
build: tsup build todisttest: run vitestlint: eslintformat: prettier
License
MIT
