From 2cb96fe81e2b89a24fe92e05c14522b11063f169 Mon Sep 17 00:00:00 2001 From: Giulio Canti Date: Tue, 21 Jan 2025 07:22:29 +0100 Subject: [PATCH] Add `Effect.transposeOption`, closes #3142 (#4284) --- .changeset/cyan-radios-relate.md | 31 ++++++++++++++ packages/effect/dtslint/Effect.ts | 10 +++++ packages/effect/src/Effect.ts | 40 ++++++++++++++++++- .../optional-wrapping-unwrapping.test.ts | 20 ++++++++++ 4 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 .changeset/cyan-radios-relate.md create mode 100644 packages/effect/test/Effect/optional-wrapping-unwrapping.test.ts diff --git a/.changeset/cyan-radios-relate.md b/.changeset/cyan-radios-relate.md new file mode 100644 index 00000000000..f30a7d949b0 --- /dev/null +++ b/.changeset/cyan-radios-relate.md @@ -0,0 +1,31 @@ +--- +"effect": minor +--- + +Add `Effect.transposeOption`, closes #3142. + +Converts an `Option` of an `Effect` into an `Effect` of an `Option`. + +**Details** + +This function transforms an `Option>` into an +`Effect, E, R>`. If the `Option` is `None`, the resulting `Effect` +will immediately succeed with a `None` value. If the `Option` is `Some`, the +inner `Effect` will be executed, and its result wrapped in a `Some`. + +**Example** + +```ts +import { Effect, Option } from "effect" + +// ┌─── Option> +// ▼ +const maybe = Option.some(Effect.succeed(42)) + +// ┌─── Effect, never, never> +// ▼ +const result = Effect.transposeOption(maybe) + +console.log(Effect.runSync(result)) +// Output: { _id: 'Option', _tag: 'Some', value: 42 } +``` diff --git a/packages/effect/dtslint/Effect.ts b/packages/effect/dtslint/Effect.ts index 2f4b02e7063..21179cdcc7e 100644 --- a/packages/effect/dtslint/Effect.ts +++ b/packages/effect/dtslint/Effect.ts @@ -1350,3 +1350,13 @@ hole< }> > >() + +// ------------------------------------------------------------------------------------- +// transposeOption +// ------------------------------------------------------------------------------------- + +// $ExpectType Effect, never, never> +Effect.transposeOption(Option.none()) + +// $ExpectType Effect, "err-1", "dep-1"> +Effect.transposeOption(Option.some(string)) diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts index 5baec3bc5d7..6ea233d61d0 100644 --- a/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -34,6 +34,7 @@ import * as defaultServices from "./internal/defaultServices.js" import * as circular from "./internal/effect/circular.js" import * as fiberRuntime from "./internal/fiberRuntime.js" import * as layer from "./internal/layer.js" +import * as option_ from "./internal/option.js" import * as query from "./internal/query.js" import * as runtime_ from "./internal/runtime.js" import * as schedule_ from "./internal/schedule.js" @@ -12811,7 +12812,7 @@ export const withParentSpan: { * ``` * * @since 2.0.0 - * @category Optional Wrapping + * @category Optional Wrapping & Unwrapping */ export const fromNullable: (value: A) => Effect, Cause.NoSuchElementException> = effect.fromNullable @@ -12866,12 +12867,47 @@ export const fromNullable: (value: A) => Effect, Cause.NoSuchE * ``` * * @since 2.0.0 - * @category Optional Wrapping + * @category Optional Wrapping & Unwrapping */ export const optionFromOptional: ( self: Effect ) => Effect, Exclude, R> = effect.optionFromOptional +/** + * Converts an `Option` of an `Effect` into an `Effect` of an `Option`. + * + * **Details** + * + * This function transforms an `Option>` into an + * `Effect, E, R>`. If the `Option` is `None`, the resulting `Effect` + * will immediately succeed with a `None` value. If the `Option` is `Some`, the + * inner `Effect` will be executed, and its result wrapped in a `Some`. + * + * @example + * ```ts + * import { Effect, Option } from "effect" + * + * // ┌─── Option> + * // ▼ + * const maybe = Option.some(Effect.succeed(42)) + * + * // ┌─── Effect, never, never> + * // ▼ + * const result = Effect.transposeOption(maybe) + * + * console.log(Effect.runSync(result)) + * // Output: { _id: 'Option', _tag: 'Some', value: 42 } + * ``` + * + * @since 3.13.0 + * @category Optional Wrapping & Unwrapping + */ +export const transposeOption = ( + self: Option.Option> +): Effect, E, R> => { + return option_.isNone(self) ? succeedNone : map(self.value, option_.some) +} + /** * @since 2.0.0 * @category Models diff --git a/packages/effect/test/Effect/optional-wrapping-unwrapping.test.ts b/packages/effect/test/Effect/optional-wrapping-unwrapping.test.ts new file mode 100644 index 00000000000..3eb88438a45 --- /dev/null +++ b/packages/effect/test/Effect/optional-wrapping-unwrapping.test.ts @@ -0,0 +1,20 @@ +import * as Effect from "effect/Effect" +import * as Option from "effect/Option" +import * as it from "effect/test/utils/extend" +import { assert, describe } from "vitest" + +describe("Effect", () => { + describe("transposeOption", () => { + it.effect("None", () => + Effect.gen(function*() { + const result = yield* Effect.transposeOption(Option.none()) + assert.ok(Option.isNone(result)) + })) + + it.effect("Some", () => + Effect.gen(function*() { + const result = yield* Effect.transposeOption(Option.some(Effect.succeed(42))) + assert.deepStrictEqual(result, Option.some(42)) + })) + }) +})