Skip to content

Commit

Permalink
feat(either): add do notation to Either (#2157)
Browse files Browse the repository at this point in the history
  • Loading branch information
successkrisz authored and mikearnaldi committed Feb 21, 2024
1 parent 54ddbb7 commit 2097739
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 53 deletions.
5 changes: 5 additions & 0 deletions .changeset/twelve-lemons-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Add Do notation methods `Do`, `bindTo`, `bind` and `let` to Either
12 changes: 1 addition & 11 deletions packages/effect/src/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, H> = {
[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
Expand Down
82 changes: 81 additions & 1 deletion packages/effect/src/Either.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -710,3 +710,83 @@ export const gen: Gen.Gen<EitherTypeLambda, Gen.Adapter<EitherTypeLambda>> = (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: {
<N extends string, K, A, E2>(
tag: Exclude<N, keyof K>,
f: (_: K) => Either<A, E2>
): <E>(self: Either<K, E>) => Either<MergeRecord<K, { [k in N]: A }>, E2 | E>
<K, E, N extends string, A, E2>(
self: Either<E, K>,
tag: Exclude<N, keyof K>,
f: (_: K) => Either<A, E2>
): Either<MergeRecord<K, { [k in N]: A }>, E2 | E>
} = dual(3, <K, E, N extends string, A, E2>(
self: Either<K, E>,
tag: Exclude<N, keyof K>,
f: (_: K) => Either<A, E2>
): Either<MergeRecord<K, { [k in N]: A }>, E2 | E> =>
flatMap(self, (k) =>
map(
f(k),
(a): MergeRecord<K, { [k in N]: A }> => ({ ...k, [tag]: a } as any)
)))

/**
* @category do notation
* @since 2.4.0
*/
export const bindTo: {
<N extends string>(tag: N): <A, E>(self: Either<A, E>) => Either<Record<N, A>, E>
<A, E, N extends string>(self: Either<A, E>, tag: N): Either<Record<N, A>, E>
} = dual(
2,
<A, E, N extends string>(self: Either<A, E>, tag: N): Either<Record<N, A>, E> =>
map(self, (a) => ({ [tag]: a } as Record<N, A>))
)

const let_: {
<N extends string, K, A>(
tag: Exclude<N, keyof K>,
f: (_: K) => A
): <E>(self: Either<K, E>) => Either<MergeRecord<K, { [k in N]: A }>, E>
<K, E, N extends string, A>(
self: Either<K, E>,
tag: Exclude<N, keyof K>,
f: (_: K) => A
): Either<MergeRecord<K, { [k in N]: A }>, E>
} = dual(3, <K, E, N extends string, A>(
self: Either<K, E>,
tag: Exclude<N, keyof K>,
f: (_: K) => A
): Either<MergeRecord<K, { [k in N]: A }>, E> =>
map(
self,
(k): MergeRecord<K, { [k in N]: A }> => ({ ...k, [tag]: f(k) } as any)
))

export {
/**
* Like bind for values
*
* @since 2.4.0
* @category do notation
*/
let_ as let
}
10 changes: 5 additions & 5 deletions packages/effect/src/STM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

/**
Expand Down Expand Up @@ -2013,24 +2013,24 @@ export const bind: {
<N extends string, K, A, E2, R2>(
tag: Exclude<N, keyof K>,
f: (_: K) => STM<A, E2, R2>
): <E, R>(self: STM<K, E, R>) => STM<Effect.MergeRecord<K, { [k in N]: A }>, E2 | E, R2 | R>
): <E, R>(self: STM<K, E, R>) => STM<MergeRecord<K, { [k in N]: A }>, E2 | E, R2 | R>
<K, E, R, N extends string, A, E2, R2>(
self: STM<K, E, R>,
tag: Exclude<N, keyof K>,
f: (_: K) => STM<A, E2, R2>
): STM<Effect.MergeRecord<K, { [k in N]: A }>, E | E2, R | R2>
): STM<MergeRecord<K, { [k in N]: A }>, E | E2, R | R2>
} = stm.bind

const let_: {
<N extends string, K, A>(
tag: Exclude<N, keyof K>,
f: (_: K) => A
): <E, R>(self: STM<K, E, R>) => STM<Effect.MergeRecord<K, { [k in N]: A }>, E, R>
): <E, R>(self: STM<K, E, R>) => STM<MergeRecord<K, { [k in N]: A }>, E, R>
<K, E, R, N extends string, A>(
self: STM<K, E, R>,
tag: Exclude<N, keyof K>,
f: (_: K) => A
): STM<Effect.MergeRecord<K, { [k in N]: A }>, E, R>
): STM<MergeRecord<K, { [k in N]: A }>, E, R>
} = stm.let_

export {
Expand Down
14 changes: 7 additions & 7 deletions packages/effect/src/Stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

/**
Expand Down Expand Up @@ -4467,15 +4467,15 @@ export const bind: {
options?:
| { readonly concurrency?: number | "unbounded" | undefined; readonly bufferSize?: number | undefined }
| undefined
): <E, R>(self: Stream<K, E, R>) => Stream<Effect.MergeRecord<K, { [k in N]: A }>, E2 | E, R2 | R>
): <E, R>(self: Stream<K, E, R>) => Stream<MergeRecord<K, { [k in N]: A }>, E2 | E, R2 | R>
<K, E, R, N extends string, A, E2, R2>(
self: Stream<K, E, R>,
tag: Exclude<N, keyof K>,
f: (_: K) => Stream<A, E2, R2>,
options?:
| { readonly concurrency?: number | "unbounded" | undefined; readonly bufferSize?: number | undefined }
| undefined
): Stream<Effect.MergeRecord<K, { [k in N]: A }>, E | E2, R | R2>
): Stream<MergeRecord<K, { [k in N]: A }>, E | E2, R | R2>
} = internal.bind

/**
Expand All @@ -4491,15 +4491,15 @@ export const bindEffect: {
options?:
| { readonly concurrency?: number | "unbounded" | undefined; readonly bufferSize?: number | undefined }
| undefined
): <E, R>(self: Stream<K, E, R>) => Stream<Effect.MergeRecord<K, { [k in N]: A }>, E2 | E, R2 | R>
): <E, R>(self: Stream<K, E, R>) => Stream<MergeRecord<K, { [k in N]: A }>, E2 | E, R2 | R>
<K, E, R, N extends string, A, E2, R2>(
self: Stream<K, E, R>,
tag: Exclude<N, keyof K>,
f: (_: K) => Effect.Effect<A, E2, R2>,
options?:
| { readonly concurrency?: number | "unbounded" | undefined; readonly unordered?: boolean | undefined }
| undefined
): Stream<Effect.MergeRecord<K, { [k in N]: A }>, E | E2, R | R2>
): Stream<MergeRecord<K, { [k in N]: A }>, E | E2, R | R2>
} = _groupBy.bindEffect

/**
Expand All @@ -4515,12 +4515,12 @@ const let_: {
<N extends string, K, A>(
tag: Exclude<N, keyof K>,
f: (_: K) => A
): <E, R>(self: Stream<K, E, R>) => Stream<Effect.MergeRecord<K, { [k in N]: A }>, E, R>
): <E, R>(self: Stream<K, E, R>) => Stream<MergeRecord<K, { [k in N]: A }>, E, R>
<K, E, R, N extends string, A>(
self: Stream<K, E, R>,
tag: Exclude<N, keyof K>,
f: (_: K) => A
): Stream<Effect.MergeRecord<K, { [k in N]: A }>, E, R>
): Stream<MergeRecord<K, { [k in N]: A }>, E, R>
} = internal.let_

export {
Expand Down
11 changes: 11 additions & 0 deletions packages/effect/src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ export type MergeRight<K, H> = Simplify<
}
>

/**
* @since 2.0.0
* @category models
*/
export type MergeRecord<K, H> = {
[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.
*
Expand Down
18 changes: 9 additions & 9 deletions packages/effect/src/internal/core-effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -371,21 +371,21 @@ export const bind: {
<N extends string, K, A, E2, R2>(
tag: Exclude<N, keyof K>,
f: (_: K) => Effect.Effect<A, E2, R2>
): <E, R>(self: Effect.Effect<K, E, R>) => Effect.Effect<Effect.MergeRecord<K, { [k in N]: A }>, E2 | E, R2 | R>
): <E, R>(self: Effect.Effect<K, E, R>) => Effect.Effect<MergeRecord<K, { [k in N]: A }>, E2 | E, R2 | R>
<K, E, R, N extends string, A, E2, R2>(
self: Effect.Effect<K, E, R>,
tag: Exclude<N, keyof K>,
f: (_: K) => Effect.Effect<A, E2, R2>
): Effect.Effect<Effect.MergeRecord<K, { [k in N]: A }>, E2 | E, R2 | R>
): Effect.Effect<MergeRecord<K, { [k in N]: A }>, E2 | E, R2 | R>
} = dual(3, <K, E, R, N extends string, A, E2, R2>(
self: Effect.Effect<K, E, R>,
tag: Exclude<N, keyof K>,
f: (_: K) => Effect.Effect<A, E2, R2>
): Effect.Effect<Effect.MergeRecord<K, { [k in N]: A }>, E2 | E, R2 | R> =>
): Effect.Effect<MergeRecord<K, { [k in N]: A }>, E2 | E, R2 | R> =>
core.flatMap(self, (k) =>
core.map(
f(k),
(a): Effect.MergeRecord<K, { [k in N]: A }> => ({ ...k, [tag]: a } as any)
(a): MergeRecord<K, { [k in N]: A }> => ({ ...k, [tag]: a } as any)
)))

/* @internal */
Expand All @@ -403,20 +403,20 @@ export const let_: {
<N extends string, K, A>(
tag: Exclude<N, keyof K>,
f: (_: K) => A
): <E, R>(self: Effect.Effect<K, E, R>) => Effect.Effect<Effect.MergeRecord<K, { [k in N]: A }>, E, R>
): <E, R>(self: Effect.Effect<K, E, R>) => Effect.Effect<MergeRecord<K, { [k in N]: A }>, E, R>
<K, E, R, N extends string, A>(
self: Effect.Effect<K, E, R>,
tag: Exclude<N, keyof K>,
f: (_: K) => A
): Effect.Effect<Effect.MergeRecord<K, { [k in N]: A }>, E, R>
): Effect.Effect<MergeRecord<K, { [k in N]: A }>, E, R>
} = dual(3, <K, E, R, N extends string, A>(
self: Effect.Effect<K, E, R>,
tag: Exclude<N, keyof K>,
f: (_: K) => A
): Effect.Effect<Effect.MergeRecord<K, { [k in N]: A }>, E, R> =>
): Effect.Effect<MergeRecord<K, { [k in N]: A }>, E, R> =>
core.map(
self,
(k): Effect.MergeRecord<K, { [k in N]: A }> => ({ ...k, [tag]: f(k) } as any)
(k): MergeRecord<K, { [k in N]: A }> => ({ ...k, [tag]: f(k) } as any)
))

/* @internal */
Expand Down
8 changes: 4 additions & 4 deletions packages/effect/src/internal/groupBy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -282,7 +282,7 @@ export const bindEffect = dual<
readonly bufferSize?: number | undefined
}
) => <E, R>(self: Stream.Stream<K, E, R>) => Stream.Stream<
Effect.MergeRecord<K, { [k in N]: A }>,
MergeRecord<K, { [k in N]: A }>,
E | E2,
R | R2
>,
Expand All @@ -295,7 +295,7 @@ export const bindEffect = dual<
readonly unordered?: boolean | undefined
}
) => Stream.Stream<
Effect.MergeRecord<K, { [k in N]: A }>,
MergeRecord<K, { [k in N]: A }>,
E | E2,
R | R2
>
Expand All @@ -311,7 +311,7 @@ export const bindEffect = dual<
mapEffectOptions(self, (k) =>
Effect.map(
f(k),
(a): Effect.MergeRecord<K, { [k in N]: A }> => ({ ...k, [tag]: a } as any)
(a): MergeRecord<K, { [k in N]: A }> => ({ ...k, [tag]: a } as any)
), options))

const mapDequeue = <A, B>(dequeue: Queue.Dequeue<A>, f: (a: A) => B): Queue.Dequeue<B> => new MapDequeue(dequeue, f)
Expand Down
16 changes: 8 additions & 8 deletions packages/effect/src/internal/stm/stm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -114,12 +114,12 @@ export const bind = dual<
<N extends string, K, A, E2, R2>(
tag: Exclude<N, keyof K>,
f: (_: K) => STM.STM<A, E2, R2>
) => <E, R>(self: STM.STM<K, E, R>) => STM.STM<Effect.MergeRecord<K, { [k in N]: A }>, E | E2, R | R2>,
) => <E, R>(self: STM.STM<K, E, R>) => STM.STM<MergeRecord<K, { [k in N]: A }>, E | E2, R | R2>,
<K, E, R, N extends string, A, E2, R2>(
self: STM.STM<K, E, R>,
tag: Exclude<N, keyof K>,
f: (_: K) => STM.STM<A, E2, R2>
) => STM.STM<Effect.MergeRecord<K, { [k in N]: A }>, E | E2, R | R2>
) => STM.STM<MergeRecord<K, { [k in N]: A }>, E | E2, R | R2>
>(3, <K, E, R, N extends string, A, E2, R2>(
self: STM.STM<K, E, R>,
tag: Exclude<N, keyof K>,
Expand All @@ -128,7 +128,7 @@ export const bind = dual<
core.flatMap(self, (k) =>
core.map(
f(k),
(a): Effect.MergeRecord<K, { [k in N]: A }> => ({ ...k, [tag]: a } as any)
(a): MergeRecord<K, { [k in N]: A }> => ({ ...k, [tag]: a } as any)
)))

/* @internal */
Expand Down Expand Up @@ -158,7 +158,7 @@ export const let_ = dual<
tag: Exclude<N, keyof K>,
f: (_: K) => A
) => <E, R>(self: STM.STM<K, E, R>) => STM.STM<
Effect.MergeRecord<K, { [k in N]: A }>,
MergeRecord<K, { [k in N]: A }>,
E,
R
>,
Expand All @@ -167,14 +167,14 @@ export const let_ = dual<
tag: Exclude<N, keyof K>,
f: (_: K) => A
) => STM.STM<
Effect.MergeRecord<K, { [k in N]: A }>,
MergeRecord<K, { [k in N]: A }>,
E,
R
>
>(3, <K, E, R, N extends string, A>(self: STM.STM<K, E, R>, tag: Exclude<N, keyof K>, f: (_: K) => A) =>
core.map(
self,
(k): Effect.MergeRecord<K, { [k in N]: A }> => ({ ...k, [tag]: f(k) } as any)
(k): MergeRecord<K, { [k in N]: A }> => ({ ...k, [tag]: f(k) } as any)
))

/** @internal */
Expand Down
Loading

0 comments on commit 2097739

Please sign in to comment.