diff --git a/.changeset/new-garlics-own.md b/.changeset/new-garlics-own.md new file mode 100644 index 00000000000..134ea792e3e --- /dev/null +++ b/.changeset/new-garlics-own.md @@ -0,0 +1,14 @@ +--- +"effect": minor +--- + +Add `Random.choice`. + +```ts +import { Random } from "effect" + +Effect.gen(function* () { + const randomItem = yield* Random.choice([1, 2, 3]) + console.log(randomItem) +}) +``` diff --git a/packages/effect/dtslint/Random.ts b/packages/effect/dtslint/Random.ts new file mode 100644 index 00000000000..c236e979249 --- /dev/null +++ b/packages/effect/dtslint/Random.ts @@ -0,0 +1,30 @@ +import type * as Array from "../src/Array.js" +import type { Chunk } from "../src/index.js" +import * as Random from "../src/Random.js" + +declare const array: Array +declare const nonEmptyArray: Array.NonEmptyArray + +// $ExpectType Effect +Random.choice(array) + +// $ExpectType Effect +Random.choice(nonEmptyArray) + +declare const readonlyArray: Array +declare const nonEmptyReadonlyArray: Array.NonEmptyArray + +// $ExpectType Effect +Random.choice(readonlyArray) + +// $ExpectType Effect +Random.choice(nonEmptyReadonlyArray) + +declare const chunk: Chunk.Chunk +declare const nonEmptyChunk: Chunk.NonEmptyChunk + +// $ExpectType Effect +Random.choice(chunk) + +// $ExpectType Effect +Random.choice(nonEmptyChunk) diff --git a/packages/effect/src/Random.ts b/packages/effect/src/Random.ts index 26b524a4e2c..233a6f0f698 100644 --- a/packages/effect/src/Random.ts +++ b/packages/effect/src/Random.ts @@ -1,11 +1,14 @@ /** * @since 2.0.0 */ +import type * as Array from "./Array.js" +import type * as Cause from "./Cause.js" import type * as Chunk from "./Chunk.js" import type * as Context from "./Context.js" import type * as Effect from "./Effect.js" import * as defaultServices from "./internal/defaultServices.js" import * as internal from "./internal/random.js" +import type * as NonEmptyIterable from "./NonEmptyIterable.js" /** * @since 2.0.0 @@ -103,6 +106,27 @@ export const nextIntBetween: (min: number, max: number) => Effect.Effect */ export const shuffle: (elements: Iterable) => Effect.Effect> = defaultServices.shuffle +/** + * Get a random element from an iterable. + * + * @example + * import { Effect, Random } from "effect" + * + * Effect.gen(function* () { + * const randomItem = yield* Random.choice([1, 2, 3]) + * console.log(randomItem) + * }) + * + * @since 3.6.0 + * @category constructors + */ +export const choice: >( + elements: Self +) => Self extends NonEmptyIterable.NonEmptyIterable ? Effect.Effect + : Self extends Array.NonEmptyReadonlyArray ? Effect.Effect + : Self extends Iterable ? Effect.Effect + : never = defaultServices.choice + /** * Retreives the `Random` service from the context and uses it to run the * specified workflow. diff --git a/packages/effect/src/internal/defaultServices.ts b/packages/effect/src/internal/defaultServices.ts index f8c6ade9064..18f96fc4b57 100644 --- a/packages/effect/src/internal/defaultServices.ts +++ b/packages/effect/src/internal/defaultServices.ts @@ -1,3 +1,4 @@ +import * as Array from "../Array.js" import type * as Chunk from "../Chunk.js" import type * as Clock from "../Clock.js" import type * as Config from "../Config.js" @@ -133,6 +134,19 @@ export const nextIntBetween = (min: number, max: number): Effect.Effect export const shuffle = (elements: Iterable): Effect.Effect> => randomWith((random) => random.shuffle(elements)) +/** @internal */ +export const choice = >( + elements: Self +) => { + const array = Array.fromIterable(elements) + return core.map( + array.length === 0 + ? core.fail(new core.NoSuchElementException("Cannot select a random element from an empty array")) + : randomWith((random) => random.nextIntBetween(0, array.length)), + (i) => array[i] + ) as any +} + // circular with Tracer /** @internal */ diff --git a/packages/effect/test/Random.test.ts b/packages/effect/test/Random.test.ts index 5bcf690a093..6d4886c10bd 100644 --- a/packages/effect/test/Random.test.ts +++ b/packages/effect/test/Random.test.ts @@ -1,12 +1,12 @@ -import { Array, Chunk, Data, Effect, Random } from "effect" -import * as it from "effect/test/utils/extend" +import { Array, Cause, Chunk, Data, Effect, Random } from "effect" +import { expect, it } from "effect/test/utils/extend" import { assert, describe } from "vitest" describe("Random", () => { it.effect("shuffle", () => - Effect.gen(function*($) { + Effect.gen(function*() { const start = Array.range(0, 100) - const end = yield* $(Random.shuffle(start)) + const end = yield* Random.shuffle(start) assert.isTrue(Chunk.every(end, (n) => n !== undefined)) assert.deepStrictEqual(start.sort(), Array.fromIterable(end).sort()) }).pipe(Effect.repeatN(100))) @@ -25,4 +25,15 @@ describe("Random", () => { assert.strictEqual(n2, n3) assert.notStrictEqual(n0, n2) })) + + it.live("choice", () => + Effect.gen(function*() { + expect(yield* Random.choice([]).pipe(Effect.flip)).toEqual(new Cause.NoSuchElementException()) + expect(yield* Random.choice([1])).toEqual(1) + + const randomItems = yield* Random.choice([1, 2, 3]).pipe(Array.replicate(100), Effect.all) + expect(Array.intersection(randomItems, [1, 2, 3]).length).toEqual(randomItems.length) + + expect(yield* Random.choice(Chunk.fromIterable([1, 2, 3]))).oneOf([1, 2, 3]) + })) })