npm package: @fuiste/optics-effect
Description: Effect bindings for @fuiste/optics using Effect.Either
Latest version: 0.1.0

# @fuiste/optics-effect

Effect bindings for `@fuiste/optics` using `effect`'s Either for safe get/set and composition.

## Installation

```bash
pnpm add @fuiste/optics-effect
# peer deps
pnpm add effect -D
pnpm add @fuiste/optics
```

## Usage

```ts
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

```ts
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

```ts
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.

```ts
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