Skip to content

Commit

Permalink
introduce bindAll
Browse files Browse the repository at this point in the history
  • Loading branch information
maksim.khramtsov authored and tim-smart committed Aug 20, 2024
1 parent aacbb7a commit f304bab
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/shiny-carpets-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": minor
---

Introduce `bindAll`
79 changes: 72 additions & 7 deletions packages/effect/src/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,18 +594,23 @@ export declare namespace All {
*/
export type ExtractMode<A> = [A] extends [{ mode: infer M }] ? M : "default"

/**
* @since 3.7.0
*/
export type Options = {
readonly concurrency?: Concurrency | undefined
readonly batching?: boolean | "inherit" | undefined
readonly discard?: boolean | undefined
readonly mode?: "default" | "validate" | "either" | undefined
readonly concurrentFinalizers?: boolean | undefined
}

/**
* @since 2.0.0
*/
export type Return<
Arg extends Iterable<EffectAny> | Record<string, EffectAny>,
O extends {
readonly concurrency?: Concurrency | undefined
readonly batching?: boolean | "inherit" | undefined
readonly discard?: boolean | undefined
readonly mode?: "default" | "validate" | "either" | undefined
readonly concurrentFinalizers?: boolean | undefined
}
O extends Options
> = [Arg] extends [ReadonlyArray<EffectAny>] ? ReturnTuple<Arg, IsDiscard<O>, ExtractMode<O>>
: [Arg] extends [Iterable<EffectAny>] ? ReturnIterable<Arg, IsDiscard<O>, ExtractMode<O>>
: [Arg] extends [Record<string, EffectAny>] ? ReturnObject<Arg, IsDiscard<O>, ExtractMode<O>>
Expand Down Expand Up @@ -3540,6 +3545,66 @@ export const bind: {
): Effect<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E1 | E2, R1 | R2>
} = effect.bind

/**
* @example
* import { Effect, pipe } from "effect"
*
* const result = pipe(
* Effect.Do,
* Effect.bind("x", () => Effect.succeed(2)),
* Effect.bindAll(({ x }) => ({
* a: Effect.succeed(x),
* b: Effect.fail('ops'),
* }), { concurrency: 2, mode: 'either' })
* )
* assert.deepStrictEqual(Effect.runSync(result), { x: 2, a: Either.right(2), b: Either.left('ops') })
*
* @see All.Options
* @category do notation
* @since 3.7.0
*/
export const bindAll: {
<
A extends object,
X extends Record<string, Effect<any, any, any>>,
O extends All.Options
>(
f: (a: A) => [Extract<keyof X, keyof A>] extends [never] ? X : `Duplicate keys`,
options?: undefined | O
): <E1, R1>(
self: Effect<A, E1, R1>
) => Effect<
{
[K in keyof X | keyof A]: K extends keyof A ? A[K] :
K extends keyof Effect.Success<
All.ReturnObject<X, All.IsDiscard<O>, All.ExtractMode<O>>
> ? Effect.Success<All.ReturnObject<X, All.IsDiscard<O>, All.ExtractMode<O>>>[K] :
never
},
| E1
| Effect.Error<All.ReturnObject<X, All.IsDiscard<O>, All.ExtractMode<O>>>,
R1 | Effect.Context<X[keyof X]>
>
<A extends object, X extends Record<string, Effect<any, any, any>>, O extends All.Options, E1, R1>(
self: Effect<A, E1, R1>,
f: (a: A) => [Extract<keyof X, keyof A>] extends [never] ? X : `Duplicate keys`,
options?: undefined | All.Options
): Effect<
{
[K in keyof X | keyof A]: K extends keyof A ? A[K] :
K extends keyof Effect.Success<
All.ReturnObject<X, All.IsDiscard<O>, All.ExtractMode<O>>
> ? Effect.Success<
All.ReturnObject<X, All.IsDiscard<O>, All.ExtractMode<O>>
>[K] :
never
},
| E1
| Effect.Error<All.ReturnObject<X, All.IsDiscard<O>, All.ExtractMode<O>>>,
R1 | Effect.Context<X[keyof X]>
>
} = effect.bindAll

/**
* The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `bind` and `let`.
*
Expand Down
69 changes: 68 additions & 1 deletion packages/effect/src/internal/core-effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as Chunk from "../Chunk.js"
import * as Clock from "../Clock.js"
import * as Context from "../Context.js"
import * as Duration from "../Duration.js"
import type * as Effect from "../Effect.js"
import * as Effect from "../Effect.js"
import type * as Fiber from "../Fiber.js"
import type * as FiberId from "../FiberId.js"
import type * as FiberRef from "../FiberRef.js"
Expand Down Expand Up @@ -407,6 +407,73 @@ export const let_: {
): Effect.Effect<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E, R>
} = doNotation.let_<Effect.EffectTypeLambda>(core.map)

/* @internal */
export const bindAll: {
<A extends object, X extends Record<string, Effect.Effect<any, any, any>>, O extends Effect.All.Options>(
f: (a: A) => [Extract<keyof X, keyof A>] extends [never] ? X : `Duplicate keys`,
options?: undefined | O
): <E1, R1>(self: Effect.Effect<A, E1, R1>) => Effect.Effect<
{
[K in keyof X | keyof A]: K extends keyof A ? A[K] :
K extends keyof Effect.Effect.Success<
Effect.All.ReturnObject<X, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>
> ? Effect.Effect.Success<
Effect.All.ReturnObject<X, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>
>[K] :
never
},
| E1
| Effect.Effect.Error<
Effect.All.ReturnObject<X, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>
>,
R1 | Effect.Effect.Context<X[keyof X]>
>
<
A extends object,
X extends Record<string, Effect.Effect<any, any, any>>,
O extends Effect.All.Options,
E1,
R1
>(
self: Effect.Effect<A, E1, R1>,
f: (a: A) => [Extract<keyof X, keyof A>] extends [never] ? X : `Duplicate keys`,
options?: undefined | Effect.All.Options
): Effect.Effect<
{
[K in keyof X | keyof A]: K extends keyof A ? A[K] :
K extends keyof Effect.Effect.Success<
Effect.All.ReturnObject<X, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>
> ? Effect.Effect.Success<
Effect.All.ReturnObject<X, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>
>[K] :
never
},
| E1
| Effect.Effect.Error<
Effect.All.ReturnObject<X, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>
>,
R1 | Effect.Effect.Context<X[keyof X]>
>
} = dual((args) => core.isEffect(args[0]), <
A extends object,
X extends Record<string, Effect.Effect<any, any, any>>,
O extends Effect.All.Options,
E1,
R1
>(
self: Effect.Effect<A, E1, R1>,
f: (a: A) => X,
options?: undefined | O
) =>
Effect.flatMap(
self,
(a) =>
(Effect.all(f(a), options) as Effect.All.ReturnObject<X, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>)
.pipe(
Effect.map((record) => Object.assign({}, a, record))
)
))

/* @internal */
export const dropUntil: {
<A, E, R>(
Expand Down
56 changes: 56 additions & 0 deletions packages/effect/test/Effect/do-notation.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as Effect from "effect/Effect"
import * as Either from "effect/Either"
import { pipe } from "effect/Function"
import * as Option from "effect/Option"
import * as Util from "effect/test/util"
import { describe, it } from "vitest"

Expand Down Expand Up @@ -44,4 +45,59 @@ describe("do notation", () => {
"left"
)
})

describe("bindAll", () => {
describe("succeed", () => {
const getTest = <O extends Effect.All.Options>(options: O) =>
Effect.Do.pipe(
Effect.bind("x", () => Effect.succeed(2)),
Effect.bindAll(({ x }) => ({
a: Effect.succeed(x),
b: Effect.succeed("ops")
}), options)
)

expectRight(getTest({ mode: "default" }), {
a: 2,
b: "ops",
x: 2
})

expectRight(getTest({ mode: "either" }), {
a: Either.right(2),
b: Either.right("ops"),
x: 2
})

expectRight(getTest({ mode: "validate" }), {
a: 2,
b: "ops",
x: 2
})
})

describe("with failure", () => {
const getTest = <O extends Effect.All.Options>(options: O) =>
Effect.Do.pipe(
Effect.bind("x", () => Effect.succeed(2)),
Effect.bindAll(({ x }) => ({
a: Effect.fail(x), // <-- fail
b: Effect.succeed("ops")
}), options)
)

expectLeft(getTest({ mode: "default" }), 2)

expectRight(getTest({ mode: "either" }), {
a: Either.left(2),
b: Either.right("ops"),
x: 2
})

expectLeft(getTest({ mode: "validate" }), {
a: Option.some(2),
b: Option.none()
})
})
})
})

0 comments on commit f304bab

Please sign in to comment.