Skip to content

Commit

Permalink
add Effect.filterEffect* apis (#4335)
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-smart committed Feb 14, 2025
1 parent 655bfe2 commit 4f810cc
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 0 deletions.
56 changes: 56 additions & 0 deletions .changeset/warm-clouds-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
"effect": minor
---

add Effect.filterEffect\* apis

#### Effect.filterEffectOrElse

Filters an effect with an effectful predicate, falling back to an alternative
effect if the predicate fails.

```ts
import { Effect, pipe } from "effect"

// Define a user interface
interface User {
readonly name: string
}

// Simulate an asynchronous authentication function
declare const auth: () => Promise<User | null>

const program = pipe(
Effect.promise(() => auth()),
// Use filterEffectOrElse with an effectful predicate
Effect.filterEffectOrElse({
predicate: (user) => Effect.succeed(user !== null),
orElse: (user) => Effect.fail(new Error(`Unauthorized user: ${user}`))
})
)
```

#### Effect.filterEffectOrFail

Filters an effect with an effectful predicate, failing with a custom error if the predicate fails.

```ts
import { Effect, pipe } from "effect"

// Define a user interface
interface User {
readonly name: string
}

// Simulate an asynchronous authentication function
declare const auth: () => Promise<User | null>

const program = pipe(
Effect.promise(() => auth()),
// Use filterEffectOrFail with an effectful predicate
Effect.filterEffectOrFail({
predicate: (user) => Effect.succeed(user !== null),
orFailWith: (user) => Effect.fail(new Error(`Unauthorized user: ${user}`))
})
)
```
107 changes: 107 additions & 0 deletions packages/effect/src/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8308,6 +8308,113 @@ export const filterOrFail: {
<A, E, R>(self: Effect<A, E, R>, predicate: Predicate<A>): Effect<A, E | Cause.NoSuchElementException, R>
} = effect.filterOrFail

/**
* Filters an effect with an effectful predicate, falling back to an alternative
* effect if the predicate fails.
*
* **Details**
*
* This function applies a predicate to the result of an effect. If the
* predicate evaluates to `false`, the effect falls back to the `orElse`
* effect. The `orElse` effect can produce an alternative value or perform
* additional computations.
*
* @example
* ```ts
* import { Effect, pipe } from "effect"
*
* // Define a user interface
* interface User {
* readonly name: string
* }
*
* // Simulate an asynchronous authentication function
* declare const auth: () => Promise<User | null>
*
* const program = pipe(
* Effect.promise(() => auth()),
* // Use filterEffectOrElse with an effectful predicate
* Effect.filterEffectOrElse({
* predicate: (user) => Effect.succeed(user !== null),
* orElse: (user) => Effect.fail(new Error(`Unauthorized user: ${user}`))
* }),
* )
* ```
*
* @since 3.13.0
* @category Filtering
*/
export const filterEffectOrElse: {
<A, E2, R2, A2, E3, R3>(
options: {
readonly predicate: (a: NoInfer<A>) => Effect<boolean, E2, R2>
readonly orElse: (a: NoInfer<A>) => Effect<A2, E3, R3>
}
): <E, R>(self: Effect<A, E, R>) => Effect<A | A2, E | E2 | E3, R | R2 | R3>
<A, E, R, E2, R2, A2, E3, R3>(
self: Effect<A, E, R>,
options: {
readonly predicate: (a: A) => Effect<boolean, E2, R2>
readonly orElse: (a: A) => Effect<A2, E3, R3>
}
): Effect<A | A2, E | E2 | E3, R | R2 | R3>
} = core.filterEffectOrElse

/**
* Filters an effect with an effectful predicate, failing with a custom error if the predicate fails.
*
* **Details**
*
* This function applies a predicate to the result of an effect. If the
* predicate evaluates to `false`, the effect fails with a custom error
* generated by the `orFailWith` function.
*
* **When to Use**
*
* This is useful for enforcing constraints and treating violations as
* recoverable errors.
*
* @example
* ```ts
* import { Effect, pipe } from "effect"
*
* // Define a user interface
* interface User {
* readonly name: string
* }
*
* // Simulate an asynchronous authentication function
* declare const auth: () => Promise<User | null>
*
* const program = pipe(
* Effect.promise(() => auth()),
* // Use filterEffectOrFail with an effectful predicate
* Effect.filterEffectOrFail({
* predicate: (user) => Effect.succeed(user !== null),
* orFailWith: () => new Error("Unauthorized")
* }),
* )
* ```
*
* @since 3.13.0
* @category Filtering
*/
export const filterEffectOrFail: {
<A, E2, R2, E3>(
options: {
readonly predicate: (a: NoInfer<A>) => Effect<boolean, E2, R2>
readonly orFailWith: (a: NoInfer<A>) => E3
}
): <E, R>(self: Effect<A, E, R>) => Effect<A, E | E2 | E3, R | R2>
<A, E, R, E2, R2, E3>(
self: Effect<A, E, R>,
options: {
readonly predicate: (a: A) => Effect<boolean, E2, R2>
readonly orFailWith: (a: A) => E3
}
): Effect<A, E | E2 | E3, R | R2>
} = core.filterEffectOrFail

/**
* Executes an effect only if the condition is `false`.
*
Expand Down
62 changes: 62 additions & 0 deletions packages/effect/src/internal/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3058,6 +3058,68 @@ export const mapInputContext = dual<
f: (context: Context.Context<R2>) => Context.Context<R>
) => contextWithEffect((context: Context.Context<R2>) => provideContext(self, f(context))))

// -----------------------------------------------------------------------------
// Filtering
// -----------------------------------------------------------------------------

/** @internal */
export const filterEffectOrElse: {
<A, E2, R2, A2, E3, R3>(
options: {
readonly predicate: (a: NoInfer<A>) => Effect.Effect<boolean, E2, R2>
readonly orElse: (a: NoInfer<A>) => Effect.Effect<A2, E3, R3>
}
): <E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A | A2, E | E2 | E3, R | R2 | R3>
<A, E, R, E2, R2, A2, E3, R3>(
self: Effect.Effect<A, E, R>,
options: {
readonly predicate: (a: A) => Effect.Effect<boolean, E2, R2>
readonly orElse: (a: A) => Effect.Effect<A2, E3, R3>
}
): Effect.Effect<A | A2, E | E2 | E3, R | R2 | R3>
} = dual(2, <A, E, R, E2, R2, A2, E3, R3>(
self: Effect.Effect<A, E, R>,
options: {
readonly predicate: (a: A) => Effect.Effect<boolean, E2, R2>
readonly orElse: (a: A) => Effect.Effect<A2, E3, R3>
}
): Effect.Effect<A | A2, E | E2 | E3, R | R2 | R3> =>
flatMap(
self,
(a) =>
flatMap(
options.predicate(a),
(pass): Effect.Effect<A | A2, E3, R3> => pass ? succeed(a) : options.orElse(a)
)
))

/** @internal */
export const filterEffectOrFail: {
<A, E2, R2, E3>(
options: {
readonly predicate: (a: NoInfer<A>) => Effect.Effect<boolean, E2, R2>
readonly orFailWith: (a: NoInfer<A>) => E3
}
): <E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E | E2 | E3, R | R2>
<A, E, R, E2, R2, E3>(
self: Effect.Effect<A, E, R>,
options: {
readonly predicate: (a: A) => Effect.Effect<boolean, E2, R2>
readonly orFailWith: (a: A) => E3
}
): Effect.Effect<A, E | E2 | E3, R | R2>
} = dual(2, <A, E, R, E2, R2, E3>(
self: Effect.Effect<A, E, R>,
options: {
readonly predicate: (a: A) => Effect.Effect<boolean, E2, R2>
readonly orFailWith: (a: A) => E3
}
): Effect.Effect<A, E | E2 | E3, R | R2> =>
filterEffectOrElse(self, {
predicate: options.predicate,
orElse: (a) => fail(options.orFailWith(a))
}))

// -----------------------------------------------------------------------------
// Tracing
// -----------------------------------------------------------------------------
Expand Down
28 changes: 28 additions & 0 deletions packages/effect/test/Effect/filtering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ describe("Effect", () => {
assertRight(goodCase, 0)
assertLeft(badCase, Either.left("predicate failed, got 1!"))
}))

it.effect("filterOrFail - without orFailWith", () =>
Effect.gen(function*() {
const goodCase = yield* pipe(
Expand All @@ -207,4 +208,31 @@ describe("Effect", () => {
deepStrictEqual(goodCaseDataFirst, 0)
deepStrictEqual(badCase, new Cause.NoSuchElementException())
}))

describe("filterEffectOrElse", () => {
it.effect("executes fallback", () =>
Effect.gen(function*() {
const result = yield* Effect.succeed(1).pipe(
Effect.filterEffectOrElse({
predicate: (n) => Effect.succeed(n === 0),
orElse: () => Effect.succeed(0)
})
)
assert.strictEqual(result, 0)
}))
})

describe("filterEffectOrFails", () => {
it.effect("executes orFailWith", () =>
Effect.gen(function*() {
const result = yield* Effect.succeed(1).pipe(
Effect.filterEffectOrElse({
predicate: (n) => Effect.succeed(n === 0),
orElse: () => Effect.fail("boom")
}),
Effect.flip
)
assert.strictEqual(result, "boom")
}))
})
})

0 comments on commit 4f810cc

Please sign in to comment.