Skip to content

Commit

Permalink
add Random.choice (#3314)
Browse files Browse the repository at this point in the history
Co-authored-by: Tim <hello@timsmart.co>
  • Loading branch information
sukovanej and tim-smart committed Jul 30, 2024
1 parent 7d02174 commit 2d09078
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 4 deletions.
14 changes: 14 additions & 0 deletions .changeset/new-garlics-own.md
Original file line number Diff line number Diff line change
@@ -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)
})
```
30 changes: 30 additions & 0 deletions packages/effect/dtslint/Random.ts
Original file line number Diff line number Diff line change
@@ -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<number>
declare const nonEmptyArray: Array.NonEmptyArray<number>

// $ExpectType Effect<number, NoSuchElementException, never>
Random.choice(array)

// $ExpectType Effect<number, never, never>
Random.choice(nonEmptyArray)

declare const readonlyArray: Array<number>
declare const nonEmptyReadonlyArray: Array.NonEmptyArray<number>

// $ExpectType Effect<number, NoSuchElementException, never>
Random.choice(readonlyArray)

// $ExpectType Effect<number, never, never>
Random.choice(nonEmptyReadonlyArray)

declare const chunk: Chunk.Chunk<number>
declare const nonEmptyChunk: Chunk.NonEmptyChunk<number>

// $ExpectType Effect<number, NoSuchElementException, never>
Random.choice(chunk)

// $ExpectType Effect<number, never, never>
Random.choice(nonEmptyChunk)
24 changes: 24 additions & 0 deletions packages/effect/src/Random.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -103,6 +106,27 @@ export const nextIntBetween: (min: number, max: number) => Effect.Effect<number>
*/
export const shuffle: <A>(elements: Iterable<A>) => Effect.Effect<Chunk.Chunk<A>> = 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: <Self extends Iterable<unknown>>(
elements: Self
) => Self extends NonEmptyIterable.NonEmptyIterable<infer A> ? Effect.Effect<A>
: Self extends Array.NonEmptyReadonlyArray<infer A> ? Effect.Effect<A>
: Self extends Iterable<infer A> ? Effect.Effect<A, Cause.NoSuchElementException>
: never = defaultServices.choice

/**
* Retreives the `Random` service from the context and uses it to run the
* specified workflow.
Expand Down
14 changes: 14 additions & 0 deletions packages/effect/src/internal/defaultServices.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -133,6 +134,19 @@ export const nextIntBetween = (min: number, max: number): Effect.Effect<number>
export const shuffle = <A>(elements: Iterable<A>): Effect.Effect<Chunk.Chunk<A>> =>
randomWith((random) => random.shuffle(elements))

/** @internal */
export const choice = <Self extends Iterable<unknown>>(
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 */
Expand Down
19 changes: 15 additions & 4 deletions packages/effect/test/Random.test.ts
Original file line number Diff line number Diff line change
@@ -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)))
Expand All @@ -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])
}))
})

0 comments on commit 2d09078

Please sign in to comment.