Skip to content

Commit

Permalink
Add do notation for Array (#2678)
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>
Co-authored-by: Giulio Canti <giulio.canti@gmail.com>
  • Loading branch information
4 people committed May 15, 2024
1 parent 6ac4847 commit 4431169
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/metal-balloons-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": minor
---

Add do notation for Array
214 changes: 214 additions & 0 deletions packages/effect/src/Array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -2111,3 +2112,216 @@ export const cartesian: {
2,
<A, B>(self: ReadonlyArray<A>, that: ReadonlyArray<B>): 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: {
<A extends object, N extends string, B>(
tag: Exclude<N, keyof A>,
f: (a: A) => ReadonlyArray<B>
): (
self: ReadonlyArray<A>
) => Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }>
<A extends object, N extends string, B>(
self: ReadonlyArray<A>,
tag: Exclude<N, keyof A>,
f: (a: A) => ReadonlyArray<B>
): Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }>
} = doNotation.bind<ReadonlyArrayTypeLambda>(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: {
<N extends string>(tag: N): <A>(self: ReadonlyArray<A>) => Array<{ [K in N]: A }>
<A, N extends string>(self: ReadonlyArray<A>, tag: N): Array<{ [K in N]: A }>
} = doNotation.bindTo<ReadonlyArrayTypeLambda>(map) as any

const let_: {
<N extends string, B, A extends object>(
tag: Exclude<N, keyof A>,
f: (a: A) => B
): (self: ReadonlyArray<A>) => Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }>
<N extends string, A extends object, B>(
self: ReadonlyArray<A>,
tag: Exclude<N, keyof A>,
f: (a: A) => B
): Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }>
} = doNotation.let_<ReadonlyArrayTypeLambda>(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
}
23 changes: 23 additions & 0 deletions packages/effect/test/Array.test.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -1222,4 +1223,26 @@ describe("ReadonlyArray", () => {
const arr: ReadonlyArray<X> = [{ 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, [])
})
})

0 comments on commit 4431169

Please sign in to comment.