From 4431169ad916605fe7f6fc0aecf544300e4c5ad1 Mon Sep 17 00:00:00 2001 From: Maxim Khramtsov Date: Fri, 3 May 2024 13:06:46 +0200 Subject: [PATCH] Add do notation for Array (#2678) Co-authored-by: maksim.khramtsov Co-authored-by: Tim Co-authored-by: Giulio Canti --- .changeset/metal-balloons-play.md | 5 + packages/effect/src/Array.ts | 214 +++++++++++++++++++++++++++++ packages/effect/test/Array.test.ts | 23 ++++ 3 files changed, 242 insertions(+) create mode 100644 .changeset/metal-balloons-play.md diff --git a/.changeset/metal-balloons-play.md b/.changeset/metal-balloons-play.md new file mode 100644 index 0000000000..796ca9febe --- /dev/null +++ b/.changeset/metal-balloons-play.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +Add do notation for Array diff --git a/packages/effect/src/Array.ts b/packages/effect/src/Array.ts index 5b5ded67e8..b3085ffa5a 100644 --- a/packages/effect/src/Array.ts +++ b/packages/effect/src/Array.ts @@ -12,6 +12,7 @@ import type { LazyArg } from "./Function.js" import { dual, identity } from "./Function.js" import type { TypeLambda } from "./HKT.js" import * as readonlyArray from "./internal/array.js" +import * as doNotation from "./internal/doNotation.js" import * as EffectIterable from "./Iterable.js" import type { Option } from "./Option.js" import * as O from "./Option.js" @@ -2111,3 +2112,216 @@ export const cartesian: { 2, (self: ReadonlyArray, that: ReadonlyArray): Array<[A, B]> => cartesianWith(self, that, (a, b) => [a, b]) ) + +// ------------------------------------------------------------------------------------- +// do notation +// ------------------------------------------------------------------------------------- + +/** + * The "do simulation" for array allows you to sequentially apply operations to the elements of arrays, just as nested loops allow you to go through all combinations of elements in an arrays. + * + * It can be used to simulate "array comprehension". + * It's a technique that allows you to create new arrays by iterating over existing ones and applying specific **conditions** or **transformations** to the elements. It's like assembling a new collection from pieces of other collections based on certain rules. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Array` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Option` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * @see {@link bindTo} + * @see {@link bind} + * @see {@link let_ let} + * + * @example + * import { Array as Arr, pipe } from "effect" + * const doResult = pipe( + * Arr.Do, + * Arr.bind("x", () => [1, 3, 5]), + * Arr.bind("y", () => [2, 4, 6]), + * Arr.filter(({ x, y }) => x < y), // condition + * Arr.map(({ x, y }) => [x, y] as const) // transformation + * ) + * assert.deepStrictEqual(doResult, [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]]) + * + * // equivalent + * const x = [1, 3, 5], + * y = [2, 4, 6], + * result = []; + * for(let i = 0; i < x.length; i++) { + * for(let j = 0; j < y.length; j++) { + * const _x = x[i], _y = y[j]; + * if(_x < _y) result.push([_x, _y] as const) + * } + * } + * + * @category do notation + * @since 3.2.0 + */ +export const Do: ReadonlyArray<{}> = of({}) + +/** + * The "do simulation" for array allows you to sequentially apply operations to the elements of arrays, just as nested loops allow you to go through all combinations of elements in an arrays. + * + * It can be used to simulate "array comprehension". + * It's a technique that allows you to create new arrays by iterating over existing ones and applying specific **conditions** or **transformations** to the elements. It's like assembling a new collection from pieces of other collections based on certain rules. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Array` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Option` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * @see {@link bindTo} + * @see {@link Do} + * @see {@link let_ let} + * + * @example + * import { Array as Arr, pipe } from "effect" + * const doResult = pipe( + * Arr.Do, + * Arr.bind("x", () => [1, 3, 5]), + * Arr.bind("y", () => [2, 4, 6]), + * Arr.filter(({ x, y }) => x < y), // condition + * Arr.map(({ x, y }) => [x, y] as const) // transformation + * ) + * assert.deepStrictEqual(doResult, [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]]) + * + * // equivalent + * const x = [1, 3, 5], + * y = [2, 4, 6], + * result = []; + * for(let i = 0; i < x.length; i++) { + * for(let j = 0; j < y.length; j++) { + * const _x = x[i], _y = y[j]; + * if(_x < _y) result.push([_x, _y] as const) + * } + * } + * + * @category do notation + * @since 3.2.0 + */ +export const bind: { + ( + tag: Exclude, + f: (a: A) => ReadonlyArray + ): ( + self: ReadonlyArray + ) => Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> + ( + self: ReadonlyArray, + tag: Exclude, + f: (a: A) => ReadonlyArray + ): Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> +} = doNotation.bind(map, flatMap) as any + +/** + * The "do simulation" for array allows you to sequentially apply operations to the elements of arrays, just as nested loops allow you to go through all combinations of elements in an arrays. + * + * It can be used to simulate "array comprehension". + * It's a technique that allows you to create new arrays by iterating over existing ones and applying specific **conditions** or **transformations** to the elements. It's like assembling a new collection from pieces of other collections based on certain rules. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Array` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Option` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * @see {@link bindTo} + * @see {@link Do} + * @see {@link let_ let} + * + * @example + * import { Array as Arr, pipe } from "effect" + * const doResult = pipe( + * Arr.Do, + * Arr.bind("x", () => [1, 3, 5]), + * Arr.bind("y", () => [2, 4, 6]), + * Arr.filter(({ x, y }) => x < y), // condition + * Arr.map(({ x, y }) => [x, y] as const) // transformation + * ) + * assert.deepStrictEqual(doResult, [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]]) + * + * // equivalent + * const x = [1, 3, 5], + * y = [2, 4, 6], + * result = []; + * for(let i = 0; i < x.length; i++) { + * for(let j = 0; j < y.length; j++) { + * const _x = x[i], _y = y[j]; + * if(_x < _y) result.push([_x, _y] as const) + * } + * } + * + * @category do notation + * @since 3.2.0 + */ +export const bindTo: { + (tag: N): (self: ReadonlyArray) => Array<{ [K in N]: A }> + (self: ReadonlyArray, tag: N): Array<{ [K in N]: A }> +} = doNotation.bindTo(map) as any + +const let_: { + ( + tag: Exclude, + f: (a: A) => B + ): (self: ReadonlyArray) => Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> + ( + self: ReadonlyArray, + tag: Exclude, + f: (a: A) => B + ): Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> +} = doNotation.let_(map) as any + +export { + /** + * The "do simulation" for array allows you to sequentially apply operations to the elements of arrays, just as nested loops allow you to go through all combinations of elements in an arrays. + * + * It can be used to simulate "array comprehension". + * It's a technique that allows you to create new arrays by iterating over existing ones and applying specific **conditions** or **transformations** to the elements. It's like assembling a new collection from pieces of other collections based on certain rules. + * + * Here's how the do simulation works: + * + * 1. Start the do simulation using the `Do` value + * 2. Within the do simulation scope, you can use the `bind` function to define variables and bind them to `Array` values + * 3. You can accumulate multiple `bind` statements to define multiple variables within the scope + * 4. Inside the do simulation scope, you can also use the `let` function to define variables and bind them to simple values + * 5. Regular `Option` functions like `map` and `filter` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope + * + * @see {@link bindTo} + * @see {@link bind} + * @see {@link Do} + * + * @example + * import { Array as Arr, pipe } from "effect" + * const doResult = pipe( + * Arr.Do, + * Arr.bind("x", () => [1, 3, 5]), + * Arr.bind("y", () => [2, 4, 6]), + * Arr.filter(({ x, y }) => x < y), // condition + * Arr.map(({ x, y }) => [x, y] as const) // transformation + * ) + * assert.deepStrictEqual(doResult, [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]]) + * + * // equivalent + * const x = [1, 3, 5], + * y = [2, 4, 6], + * result = []; + * for(let i = 0; i < x.length; i++) { + * for(let j = 0; j < y.length; j++) { + * const _x = x[i], _y = y[j]; + * if(_x < _y) result.push([_x, _y] as const) + * } + * } + * + * @category do notation + * @since 3.2.0 + */ + let_ as let +} diff --git a/packages/effect/test/Array.test.ts b/packages/effect/test/Array.test.ts index b157c6bde9..f7e8553fbf 100644 --- a/packages/effect/test/Array.test.ts +++ b/packages/effect/test/Array.test.ts @@ -1,4 +1,5 @@ import { deepStrictEqual, double, strictEqual } from "effect-test/util" +import * as Util from "effect-test/util" import * as RA from "effect/Array" import * as E from "effect/Either" import { identity, pipe } from "effect/Function" @@ -1222,4 +1223,26 @@ describe("ReadonlyArray", () => { const arr: ReadonlyArray = [{ a: "a", b: 2 }, { a: "b", b: 1 }] expect(RA.sortWith(arr, (x) => x.b, Order.number)).toEqual([{ a: "b", b: 1 }, { a: "a", b: 2 }]) }) + + it("Do notation", () => { + const _do = RA.Do + Util.deepStrictEqual(_do, RA.of({})) + + const doA = RA.bind(_do, "a", () => ["a"]) + Util.deepStrictEqual(doA, RA.of({ a: "a" })) + + const doAB = RA.bind(doA, "b", (x) => ["b", x.a + "b"]) + Util.deepStrictEqual(doAB, [ + { a: "a", b: "b" }, + { a: "a", b: "ab" } + ]) + const doABC = RA.let(doAB, "c", (x) => [x.a, x.b, x.a + x.b]) + Util.deepStrictEqual(doABC, [ + { a: "a", b: "b", c: ["a", "b", "ab"] }, + { a: "a", b: "ab", c: ["a", "ab", "aab"] } + ]) + + const doABCD = RA.bind(doABC, "d", () => RA.empty()) + Util.deepStrictEqual(doABCD, []) + }) })