From 20977393d2383bff709304e81ec7d51cafd57108 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 17 Feb 2024 06:40:58 +0000 Subject: [PATCH] feat(either): add do notation to Either (#2157) --- .changeset/twelve-lemons-fetch.md | 5 ++ packages/effect/src/Effect.ts | 12 +-- packages/effect/src/Either.ts | 82 ++++++++++++++++++++- packages/effect/src/STM.ts | 10 +-- packages/effect/src/Stream.ts | 14 ++-- packages/effect/src/Types.ts | 11 +++ packages/effect/src/internal/core-effect.ts | 18 ++--- packages/effect/src/internal/groupBy.ts | 8 +- packages/effect/src/internal/stm/stm.ts | 16 ++-- packages/effect/src/internal/stream.ts | 16 ++-- packages/effect/test/Either.test.ts | 47 ++++++++++++ 11 files changed, 186 insertions(+), 53 deletions(-) create mode 100644 .changeset/twelve-lemons-fetch.md diff --git a/.changeset/twelve-lemons-fetch.md b/.changeset/twelve-lemons-fetch.md new file mode 100644 index 0000000000..63f885be1a --- /dev/null +++ b/.changeset/twelve-lemons-fetch.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add Do notation methods `Do`, `bindTo`, `bind` and `let` to Either diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts index e4240827d4..48caf78525 100644 --- a/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -53,23 +53,13 @@ import * as Scheduler from "./Scheduler.js" import type * as Scope from "./Scope.js" import type * as Supervisor from "./Supervisor.js" import type * as Tracer from "./Tracer.js" -import type { Concurrency, Covariant, NoInfer } from "./Types.js" +import type { Concurrency, Covariant, MergeRecord, NoInfer } from "./Types.js" import type * as Unify from "./Unify.js" // ------------------------------------------------------------------------------------- // models // ------------------------------------------------------------------------------------- -/** - * @since 2.0.0 - */ -export type MergeRecord = { - [k in keyof K | keyof H]: k extends keyof K ? K[k] - : k extends keyof H ? H[k] - : never -} extends infer X ? X - : never - /** * @since 2.0.0 * @category symbols diff --git a/packages/effect/src/Either.ts b/packages/effect/src/Either.ts index 4d7d248d8e..ce7a7f5aee 100644 --- a/packages/effect/src/Either.ts +++ b/packages/effect/src/Either.ts @@ -12,7 +12,7 @@ import type { Option } from "./Option.js" import type { Pipeable } from "./Pipeable.js" import type { Predicate, Refinement } from "./Predicate.js" import { isFunction } from "./Predicate.js" -import type { Covariant, NoInfer } from "./Types.js" +import type { Covariant, MergeRecord, NoInfer } from "./Types.js" import type * as Unify from "./Unify.js" import * as Gen from "./Utils.js" @@ -710,3 +710,83 @@ export const gen: Gen.Gen> = (f) return right(state.value) } } + +// ------------------------------------------------------------------------------------- +// do notation +// ------------------------------------------------------------------------------------- + +/** + * @since 2.4.0 + * @category do notation + */ +export const Do: Either<{}> = right({}) + +/** + * Binds an effectful value in a `do` scope + * + * @since 2.4.0 + * @category do notation + */ +export const bind: { + ( + tag: Exclude, + f: (_: K) => Either + ): (self: Either) => Either, E2 | E> + ( + self: Either, + tag: Exclude, + f: (_: K) => Either + ): Either, E2 | E> +} = dual(3, ( + self: Either, + tag: Exclude, + f: (_: K) => Either +): Either, E2 | E> => + flatMap(self, (k) => + map( + f(k), + (a): MergeRecord => ({ ...k, [tag]: a } as any) + ))) + +/** + * @category do notation + * @since 2.4.0 + */ +export const bindTo: { + (tag: N): (self: Either) => Either, E> + (self: Either, tag: N): Either, E> +} = dual( + 2, + (self: Either, tag: N): Either, E> => + map(self, (a) => ({ [tag]: a } as Record)) +) + +const let_: { + ( + tag: Exclude, + f: (_: K) => A + ): (self: Either) => Either, E> + ( + self: Either, + tag: Exclude, + f: (_: K) => A + ): Either, E> +} = dual(3, ( + self: Either, + tag: Exclude, + f: (_: K) => A +): Either, E> => + map( + self, + (k): MergeRecord => ({ ...k, [tag]: f(k) } as any) + )) + +export { + /** + * Like bind for values + * + * @since 2.4.0 + * @category do notation + */ + let_ as let +} diff --git a/packages/effect/src/STM.ts b/packages/effect/src/STM.ts index 64de667fa6..906d9b14dd 100644 --- a/packages/effect/src/STM.ts +++ b/packages/effect/src/STM.ts @@ -14,7 +14,7 @@ import * as stm from "./internal/stm/stm.js" import type * as Option from "./Option.js" import type { Pipeable } from "./Pipeable.js" import type { Predicate, Refinement } from "./Predicate.js" -import type { Covariant, NoInfer } from "./Types.js" +import type { Covariant, MergeRecord, NoInfer } from "./Types.js" import type * as Unify from "./Unify.js" /** @@ -2013,24 +2013,24 @@ export const bind: { ( tag: Exclude, f: (_: K) => STM - ): (self: STM) => STM, E2 | E, R2 | R> + ): (self: STM) => STM, E2 | E, R2 | R> ( self: STM, tag: Exclude, f: (_: K) => STM - ): STM, E | E2, R | R2> + ): STM, E | E2, R | R2> } = stm.bind const let_: { ( tag: Exclude, f: (_: K) => A - ): (self: STM) => STM, E, R> + ): (self: STM) => STM, E, R> ( self: STM, tag: Exclude, f: (_: K) => A - ): STM, E, R> + ): STM, E, R> } = stm.let_ export { diff --git a/packages/effect/src/Stream.ts b/packages/effect/src/Stream.ts index 6e9d90b37a..1ccd80b98f 100644 --- a/packages/effect/src/Stream.ts +++ b/packages/effect/src/Stream.ts @@ -29,7 +29,7 @@ import type * as Emit from "./StreamEmit.js" import type * as HaltStrategy from "./StreamHaltStrategy.js" import type * as Take from "./Take.js" import type * as Tracer from "./Tracer.js" -import type { Covariant, NoInfer } from "./Types.js" +import type { Covariant, MergeRecord, NoInfer } from "./Types.js" import type * as Unify from "./Unify.js" /** @@ -4467,7 +4467,7 @@ export const bind: { options?: | { readonly concurrency?: number | "unbounded" | undefined; readonly bufferSize?: number | undefined } | undefined - ): (self: Stream) => Stream, E2 | E, R2 | R> + ): (self: Stream) => Stream, E2 | E, R2 | R> ( self: Stream, tag: Exclude, @@ -4475,7 +4475,7 @@ export const bind: { options?: | { readonly concurrency?: number | "unbounded" | undefined; readonly bufferSize?: number | undefined } | undefined - ): Stream, E | E2, R | R2> + ): Stream, E | E2, R | R2> } = internal.bind /** @@ -4491,7 +4491,7 @@ export const bindEffect: { options?: | { readonly concurrency?: number | "unbounded" | undefined; readonly bufferSize?: number | undefined } | undefined - ): (self: Stream) => Stream, E2 | E, R2 | R> + ): (self: Stream) => Stream, E2 | E, R2 | R> ( self: Stream, tag: Exclude, @@ -4499,7 +4499,7 @@ export const bindEffect: { options?: | { readonly concurrency?: number | "unbounded" | undefined; readonly unordered?: boolean | undefined } | undefined - ): Stream, E | E2, R | R2> + ): Stream, E | E2, R | R2> } = _groupBy.bindEffect /** @@ -4515,12 +4515,12 @@ const let_: { ( tag: Exclude, f: (_: K) => A - ): (self: Stream) => Stream, E, R> + ): (self: Stream) => Stream, E, R> ( self: Stream, tag: Exclude, f: (_: K) => A - ): Stream, E, R> + ): Stream, E, R> } = internal.let_ export { diff --git a/packages/effect/src/Types.ts b/packages/effect/src/Types.ts index 89838b052b..9f7abf2d08 100644 --- a/packages/effect/src/Types.ts +++ b/packages/effect/src/Types.ts @@ -114,6 +114,17 @@ export type MergeRight = Simplify< } > +/** + * @since 2.0.0 + * @category models + */ +export type MergeRecord = { + [k in keyof K | keyof H]: k extends keyof K ? K[k] + : k extends keyof H ? H[k] + : never +} extends infer X ? X + : never + /** * Describes the concurrency to use when executing multiple Effect's. * diff --git a/packages/effect/src/internal/core-effect.ts b/packages/effect/src/internal/core-effect.ts index c9a3b18c44..df4a819894 100644 --- a/packages/effect/src/internal/core-effect.ts +++ b/packages/effect/src/internal/core-effect.ts @@ -25,7 +25,7 @@ import * as ReadonlyArray from "../ReadonlyArray.js" import * as Ref from "../Ref.js" import type * as runtimeFlagsPatch from "../RuntimeFlagsPatch.js" import * as Tracer from "../Tracer.js" -import type { NoInfer } from "../Types.js" +import type { MergeRecord, NoInfer } from "../Types.js" import * as internalCause from "./cause.js" import * as core from "./core.js" import * as defaultServices from "./defaultServices.js" @@ -371,21 +371,21 @@ export const bind: { ( tag: Exclude, f: (_: K) => Effect.Effect - ): (self: Effect.Effect) => Effect.Effect, E2 | E, R2 | R> + ): (self: Effect.Effect) => Effect.Effect, E2 | E, R2 | R> ( self: Effect.Effect, tag: Exclude, f: (_: K) => Effect.Effect - ): Effect.Effect, E2 | E, R2 | R> + ): Effect.Effect, E2 | E, R2 | R> } = dual(3, ( self: Effect.Effect, tag: Exclude, f: (_: K) => Effect.Effect -): Effect.Effect, E2 | E, R2 | R> => +): Effect.Effect, E2 | E, R2 | R> => core.flatMap(self, (k) => core.map( f(k), - (a): Effect.MergeRecord => ({ ...k, [tag]: a } as any) + (a): MergeRecord => ({ ...k, [tag]: a } as any) ))) /* @internal */ @@ -403,20 +403,20 @@ export const let_: { ( tag: Exclude, f: (_: K) => A - ): (self: Effect.Effect) => Effect.Effect, E, R> + ): (self: Effect.Effect) => Effect.Effect, E, R> ( self: Effect.Effect, tag: Exclude, f: (_: K) => A - ): Effect.Effect, E, R> + ): Effect.Effect, E, R> } = dual(3, ( self: Effect.Effect, tag: Exclude, f: (_: K) => A -): Effect.Effect, E, R> => +): Effect.Effect, E, R> => core.map( self, - (k): Effect.MergeRecord => ({ ...k, [tag]: f(k) } as any) + (k): MergeRecord => ({ ...k, [tag]: f(k) } as any) )) /* @internal */ diff --git a/packages/effect/src/internal/groupBy.ts b/packages/effect/src/internal/groupBy.ts index 2fe4c3e365..ab87cda189 100644 --- a/packages/effect/src/internal/groupBy.ts +++ b/packages/effect/src/internal/groupBy.ts @@ -13,7 +13,7 @@ import * as Queue from "../Queue.js" import * as Ref from "../Ref.js" import type * as Stream from "../Stream.js" import type * as Take from "../Take.js" -import type { NoInfer } from "../Types.js" +import type { MergeRecord, NoInfer } from "../Types.js" import * as channel from "./channel.js" import * as channelExecutor from "./channel/channelExecutor.js" import * as core from "./core-stream.js" @@ -282,7 +282,7 @@ export const bindEffect = dual< readonly bufferSize?: number | undefined } ) => (self: Stream.Stream) => Stream.Stream< - Effect.MergeRecord, + MergeRecord, E | E2, R | R2 >, @@ -295,7 +295,7 @@ export const bindEffect = dual< readonly unordered?: boolean | undefined } ) => Stream.Stream< - Effect.MergeRecord, + MergeRecord, E | E2, R | R2 > @@ -311,7 +311,7 @@ export const bindEffect = dual< mapEffectOptions(self, (k) => Effect.map( f(k), - (a): Effect.MergeRecord => ({ ...k, [tag]: a } as any) + (a): MergeRecord => ({ ...k, [tag]: a } as any) ), options)) const mapDequeue = (dequeue: Queue.Dequeue, f: (a: A) => B): Queue.Dequeue => new MapDequeue(dequeue, f) diff --git a/packages/effect/src/internal/stm/stm.ts b/packages/effect/src/internal/stm/stm.ts index b732a815d1..1c1b86c86e 100644 --- a/packages/effect/src/internal/stm/stm.ts +++ b/packages/effect/src/internal/stm/stm.ts @@ -8,11 +8,11 @@ import type * as FiberId from "../../FiberId.js" import type { LazyArg } from "../../Function.js" import { constFalse, constTrue, constVoid, dual, identity, pipe } from "../../Function.js" import * as Option from "../../Option.js" -import * as predicate from "../../Predicate.js" import type { Predicate, Refinement } from "../../Predicate.js" +import * as predicate from "../../Predicate.js" import * as RA from "../../ReadonlyArray.js" import type * as STM from "../../STM.js" -import type { NoInfer } from "../../Types.js" +import type { MergeRecord, NoInfer } from "../../Types.js" import * as effectCore from "../core.js" import * as SingleShotGen from "../singleShotGen.js" import * as core from "./core.js" @@ -114,12 +114,12 @@ export const bind = dual< ( tag: Exclude, f: (_: K) => STM.STM - ) => (self: STM.STM) => STM.STM, E | E2, R | R2>, + ) => (self: STM.STM) => STM.STM, E | E2, R | R2>, ( self: STM.STM, tag: Exclude, f: (_: K) => STM.STM - ) => STM.STM, E | E2, R | R2> + ) => STM.STM, E | E2, R | R2> >(3, ( self: STM.STM, tag: Exclude, @@ -128,7 +128,7 @@ export const bind = dual< core.flatMap(self, (k) => core.map( f(k), - (a): Effect.MergeRecord => ({ ...k, [tag]: a } as any) + (a): MergeRecord => ({ ...k, [tag]: a } as any) ))) /* @internal */ @@ -158,7 +158,7 @@ export const let_ = dual< tag: Exclude, f: (_: K) => A ) => (self: STM.STM) => STM.STM< - Effect.MergeRecord, + MergeRecord, E, R >, @@ -167,14 +167,14 @@ export const let_ = dual< tag: Exclude, f: (_: K) => A ) => STM.STM< - Effect.MergeRecord, + MergeRecord, E, R > >(3, (self: STM.STM, tag: Exclude, f: (_: K) => A) => core.map( self, - (k): Effect.MergeRecord => ({ ...k, [tag]: f(k) } as any) + (k): MergeRecord => ({ ...k, [tag]: f(k) } as any) )) /** @internal */ diff --git a/packages/effect/src/internal/stream.ts b/packages/effect/src/internal/stream.ts index cbd7206674..9b69abb45a 100644 --- a/packages/effect/src/internal/stream.ts +++ b/packages/effect/src/internal/stream.ts @@ -10,8 +10,8 @@ import * as Either from "../Either.js" import * as Equal from "../Equal.js" import * as Exit from "../Exit.js" import * as Fiber from "../Fiber.js" -import { constTrue, dual, identity, pipe } from "../Function.js" import type { LazyArg } from "../Function.js" +import { constTrue, dual, identity, pipe } from "../Function.js" import * as Layer from "../Layer.js" import * as MergeDecision from "../MergeDecision.js" import * as Option from "../Option.js" @@ -31,7 +31,7 @@ import * as HaltStrategy from "../StreamHaltStrategy.js" import type * as Take from "../Take.js" import type * as Tracer from "../Tracer.js" import * as Tuple from "../Tuple.js" -import type { NoInfer } from "../Types.js" +import type { MergeRecord, NoInfer } from "../Types.js" import * as channel from "./channel.js" import * as channelExecutor from "./channel/channelExecutor.js" import * as MergeStrategy from "./channel/mergeStrategy.js" @@ -8018,7 +8018,7 @@ export const bind = dual< readonly bufferSize?: number | undefined } ) => (self: Stream.Stream) => Stream.Stream< - Effect.MergeRecord, + MergeRecord, E | E2, R | R2 >, @@ -8031,7 +8031,7 @@ export const bind = dual< readonly bufferSize?: number | undefined } ) => Stream.Stream< - Effect.MergeRecord, + MergeRecord, E | E2, R | R2 > @@ -8047,7 +8047,7 @@ export const bind = dual< flatMap(self, (k) => map( f(k), - (a): Effect.MergeRecord => ({ ...k, [tag]: a } as any) + (a): MergeRecord => ({ ...k, [tag]: a } as any) ), options)) /* @internal */ @@ -8077,7 +8077,7 @@ export const let_ = dual< tag: Exclude, f: (_: K) => A ) => (self: Stream.Stream) => Stream.Stream< - Effect.MergeRecord, + MergeRecord, E, R >, @@ -8086,14 +8086,14 @@ export const let_ = dual< tag: Exclude, f: (_: K) => A ) => Stream.Stream< - Effect.MergeRecord, + MergeRecord, E, R > >(3, (self: Stream.Stream, tag: Exclude, f: (_: K) => A) => map( self, - (k): Effect.MergeRecord => ({ ...k, [tag]: f(k) } as any) + (k): MergeRecord => ({ ...k, [tag]: f(k) } as any) )) // Circular with Channel diff --git a/packages/effect/test/Either.test.ts b/packages/effect/test/Either.test.ts index 637197c1ab..f8448f0239 100644 --- a/packages/effect/test/Either.test.ts +++ b/packages/effect/test/Either.test.ts @@ -317,4 +317,51 @@ describe("Either", () => { Util.deepStrictEqual(pipe(Either.left("a"), Either.orElse(() => Either.right(2))), Either.right(2)) Util.deepStrictEqual(pipe(Either.left("a"), Either.orElse(() => Either.left("b"))), Either.left("b")) }) + + it("Do", () => { + Util.deepStrictEqual(Either.Do, Either.right({})) + }) + + it("bindTo", () => { + Util.deepStrictEqual( + pipe( + Either.right(1), + Either.bindTo("a") + ), + Either.right({ a: 1 }) + ) + Util.deepStrictEqual( + pipe( + Either.left("b"), + Either.bindTo("a") + ), + Either.left("b") + ) + }) + + it("bind", () => { + Util.deepStrictEqual( + pipe(Either.right(1), Either.bindTo("a"), Either.bind("b", ({ a }) => Either.right(a + 1))), + Either.right({ a: 1, b: 2 }) + ) + Util.deepStrictEqual( + pipe(Either.right(1), Either.bindTo("a"), Either.bind("b", () => Either.left("c"))), + Either.left("c") + ) + Util.deepStrictEqual( + pipe(Either.left("d"), Either.bindTo("a"), Either.bind("b", () => Either.right(2))), + Either.left("d") + ) + }) + + it("let", () => { + Util.deepStrictEqual( + pipe(Either.Do, Either.bind("a", () => Either.right(1)), Either.let("b", ({ a }) => a + 1)), + Either.right({ a: 1, b: 2 }) + ) + Util.deepStrictEqual( + pipe(Either.left("d"), Either.bindTo("a"), Either.let("b", () => Either.right(2))), + Either.left("d") + ) + }) })