Skip to content

Commit

Permalink
Introduce bindAll (#3486)
Browse files Browse the repository at this point in the history
Co-authored-by: maksim.khramtsov <maksim.khramtsov@btsdigital.kz>
Co-authored-by: Tim <hello@timsmart.co>
  • Loading branch information
3 people committed Aug 30, 2024
1 parent db89601 commit 8356321
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 0 deletions.
29 changes: 29 additions & 0 deletions .changeset/shiny-carpets-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"effect": minor
---

add `Effect.bindAll` api

This api allows you to combine `Effect.all` with `Effect.bind`. It is useful
when you want to concurrently run multiple effects and then combine their
results in a Do notation pipeline.

```ts
import { Effect } from "effect";

const result = Effect.Do.pipe(
Effect.bind("x", () => Effect.succeed(2)),
Effect.bindAll(
({ x }) => ({
a: Effect.succeed(x + 1),
b: Effect.succeed("foo"),
}),
{ concurrency: 2 },
),
);
assert.deepStrictEqual(Effect.runSync(result), {
x: 2,
a: 3,
b: "foo",
});
```
72 changes: 72 additions & 0 deletions packages/effect/src/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3540,6 +3540,78 @@ export const bind: {
): Effect<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E1 | E2, R1 | R2>
} = effect.bind

/**
* `bindAll` combines `Effect.all` with `Effect.bind`. It is useful
* when you want to concurrently run multiple effects and then combine their
* results in a Do notation pipeline.
*
* @example
* import { Effect, Either, pipe } from "effect"
*
* const result = pipe(
* Effect.Do,
* Effect.bind("x", () => Effect.succeed(2)),
* Effect.bindAll(({ x }) => ({
* a: Effect.succeed(x),
* b: Effect.fail("oops"),
* }), { concurrency: 2, mode: "either" })
* )
* assert.deepStrictEqual(Effect.runSync(result), { x: 2, a: Either.right(2), b: Either.left("oops") })
*
* @category do notation
* @since 3.7.0
*/
export const bindAll: {
<
A extends object,
X extends Record<string, Effect<any, any, any>>,
O extends {
readonly concurrency?: Concurrency | undefined
readonly batching?: boolean | "inherit" | undefined
readonly mode?: "default" | "validate" | "either" | undefined
readonly concurrentFinalizers?: boolean | undefined
}
>(
f: (a: A) => [Extract<keyof X, keyof A>] extends [never] ? X : `Duplicate keys`,
options?: undefined | O
): <E1, R1>(
self: Effect<A, E1, R1>
) => [All.ReturnObject<X, false, All.ExtractMode<O>>] extends [Effect<infer Success, infer Error, infer Context>]
? Effect<
{ [K in keyof A | keyof Success]: K extends keyof A ? A[K] : K extends keyof Success ? Success[K] : never },
E1 | Error,
R1 | Context
>
: never
<
A extends object,
X extends Record<string, Effect<any, any, any>>,
O extends {
readonly concurrency?: Concurrency | undefined
readonly batching?: boolean | "inherit" | undefined
readonly mode?: "default" | "validate" | "either" | undefined
readonly concurrentFinalizers?: boolean | undefined
},
E1,
R1
>(
self: Effect<A, E1, R1>,
f: (a: A) => [Extract<keyof X, keyof A>] extends [never] ? X : `Duplicate keys`,
options?: undefined | {
readonly concurrency?: Concurrency | undefined
readonly batching?: boolean | "inherit" | undefined
readonly mode?: "default" | "validate" | "either" | undefined
readonly concurrentFinalizers?: boolean | undefined
}
): [All.ReturnObject<X, false, All.ExtractMode<O>>] extends [Effect<infer Success, infer Error, infer Context>]
? Effect<
{ [K in keyof A | keyof Success]: K extends keyof A ? A[K] : K extends keyof Success ? Success[K] : never },
E1 | Error,
R1 | Context
>
: never
} = circular.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
91 changes: 91 additions & 0 deletions packages/effect/src/internal/effect/circular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { currentScheduler } from "../../Scheduler.js"
import type * as Scope from "../../Scope.js"
import type * as Supervisor from "../../Supervisor.js"
import type * as Synchronized from "../../SynchronizedRef.js"
import type * as Types from "../../Types.js"
import * as internalCause from "../cause.js"
import * as effect from "../core-effect.js"
import * as core from "../core.js"
Expand Down Expand Up @@ -704,3 +705,93 @@ export const zipWithFiber = dual<
return pipeArguments(this, arguments)
}
}))

/* @internal */
export const bindAll: {
<
A extends object,
X extends Record<string, Effect.Effect<any, any, any>>,
O extends {
readonly concurrency?: Types.Concurrency | undefined
readonly batching?: boolean | "inherit" | undefined
readonly mode?: "default" | "validate" | "either" | undefined
readonly concurrentFinalizers?: boolean | undefined
}
>(
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.All.ReturnObject<X, false, Effect.All.ExtractMode<O>>] extends
[Effect.Effect<infer Success, infer Error, infer Context>] ? Effect.Effect<
{
[K in keyof A | keyof Success]: K extends keyof A ? A[K]
: K extends keyof Success ? Success[K]
: never
},
| E1
| Error,
R1 | Context
>
: never

<
A extends object,
X extends Record<string, Effect.Effect<any, any, any>>,
O extends {
readonly concurrency?: Types.Concurrency | undefined
readonly batching?: boolean | "inherit" | undefined
readonly mode?: "default" | "validate" | "either" | undefined
readonly concurrentFinalizers?: boolean | undefined
},
E1,
R1
>(
self: Effect.Effect<A, E1, R1>,
f: (a: A) => [Extract<keyof X, keyof A>] extends [never] ? X : `Duplicate keys`,
options?: undefined | {
readonly concurrency?: Types.Concurrency | undefined
readonly batching?: boolean | "inherit" | undefined
readonly mode?: "default" | "validate" | "either" | undefined
readonly concurrentFinalizers?: boolean | undefined
}
): [Effect.All.ReturnObject<X, false, Effect.All.ExtractMode<O>>] extends
[Effect.Effect<infer Success, infer Error, infer Context>] ? Effect.Effect<
{
[K in keyof A | keyof Success]: K extends keyof A ? A[K]
: K extends keyof Success ? Success[K]
: never
},
| E1
| Error,
R1 | Context
>
: never
} = dual((args) => core.isEffect(args[0]), <
A extends object,
X extends Record<string, Effect.Effect<any, any, any>>,
O extends {
readonly concurrency?: Types.Concurrency | undefined
readonly batching?: boolean | "inherit" | undefined
readonly mode?: "default" | "validate" | "either" | undefined
readonly concurrentFinalizers?: boolean | undefined
},
E1,
R1
>(
self: Effect.Effect<A, E1, R1>,
f: (a: A) => X,
options?: undefined | O
) =>
core.flatMap(
self,
(a) =>
(fiberRuntime.all(f(a), options) as Effect.All.ReturnObject<
X,
Effect.All.IsDiscard<O>,
Effect.All.ExtractMode<O>
>)
.pipe(
core.map((record) => Object.assign({}, a, record))
)
))
55 changes: 55 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,58 @@ describe("do notation", () => {
"left"
)
})

describe("bindAll", () => {
it("succeed", () => {
const getTest = <O extends { mode: "default" | "either" | "validate" }>(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
})
})

it("with failure", () => {
const getTest = <O extends { mode: "default" | "either" | "validate" }>(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 8356321

Please sign in to comment.