Skip to content

Fuiste/optics-effect

Repository files navigation

@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/optics

Usage

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 to dist
  • test: run vitest
  • lint: eslint
  • format: prettier

License

MIT

About

Effect bingings for @fuiste/optics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published