- Introduction & Core Concepts
- Basic Usage Patterns
- Real-World Examples
- Advanced Features Deep Dive
- Practical Patterns & Best Practices
- Integration Examples
Randomness in JavaScript applications presents unique challenges, especially when you need reproducible, testable random generation. The traditional Math.random() approach lacks control and predictability:
// Traditional approach - non-reproducible random generation
function generateTestData(): User[] {
const users: User[] = [];
const count = Math.floor(Math.random() * 10) + 5; // 5-14 users
for (let i = 0; i < count; i++) {
users.push({
id: Math.random().toString(36).substring(7),
name: `User${Math.floor(Math.random() * 1000)}`,
age: Math.floor(Math.random() * 50) + 18,
premium: Math.random() > 0.5,
score: Math.random() * 100
});
}
return users;
}
// Testing is problematic - results change every run
describe('UserService', () => {
it('should process users correctly', () => {
const users = generateTestData();
// Test fails randomly because data is different each time
const result = processUsers(users);
expect(result.totalScore).toBeGreaterThan(0); // Flaky!
});
});
// Seeded random with manual state management
class SeededRandom {
private seed: number;
constructor(seed: number) {
this.seed = seed;
}
next(): number {
// LCG algorithm
this.seed = (this.seed * 1664525 + 1013904223) % 4294967296;
return this.seed / 4294967296;
}
}
// Passing random state everywhere is cumbersome
function generateSeededData(random: SeededRandom): User[] {
const count = Math.floor(random.next() * 10) + 5;
const users: User[] = [];
for (let i = 0; i < count; i++) {
users.push({
id: generateId(random), // Need to pass random everywhere
name: generateName(random),
age: generateAge(random),
premium: random.next() > 0.5,
score: random.next() * 100
});
}
return users;
}This approach leads to:
- Non-reproducible tests - Test results vary between runs, making debugging difficult
- State management complexity - Manually passing random generators through function calls
- Limited distributions - Only uniform distribution, no Gaussian, exponential, etc.
Effect's Random module provides a composable, reproducible random generation system that integrates seamlessly with the Effect ecosystem:
import { Effect, Random } from "effect"
// Reproducible random generation with automatic state management
const generateTestData = Effect.gen(function* () {
const count = yield* Random.nextIntBetween(5, 15)
const users = yield* Effect.all(
Array.from({ length: count }, () =>
Effect.gen(function* () {
return {
id: yield* randomId(),
name: yield* randomName(),
age: yield* Random.nextIntBetween(18, 68),
premium: yield* Random.nextBoolean,
score: yield* Random.next
}
})
)
)
return users
})
// Tests are perfectly reproducible
test('UserService', () => {
const result = Effect.runSync(
generateTestData.pipe(
Effect.map(processUsers),
Effect.withRandom(Random.makeDeterministic("test-seed"))
)
)
// Always produces the same result for the same seed
expect(result.totalScore).toBe(423.67)
})Random Service: Effect's Random is provided as a service, allowing dependency injection and easy testing with deterministic seeds.
Deterministic Generation: Every random operation is pure and reproducible when using the same seed, perfect for testing and debugging.
Distribution Support: Built-in support for various probability distributions beyond uniform random.
import { Effect, Random, Console } from "effect"
// Generate different types of random values
const randomValues = Effect.gen(function* () {
// Random number between 0 and 1
const float = yield* Random.next
// Random integer in range
const int = yield* Random.nextIntBetween(1, 100)
// Random boolean
const bool = yield* Random.nextBoolean
// Random element from array
const colors = ["red", "green", "blue", "yellow"]
const color = yield* Random.choice(colors)
yield* Console.log({
float,
int,
bool,
color
})
})
// Run with default random
Effect.runPromise(randomValues)
// Run with seeded random for reproducibility
Effect.runPromise(
randomValues.pipe(
Effect.withRandom(Random.makeDeterministic("my-seed"))
)
)import { Effect, Random } from "effect"
const distributions = Effect.gen(function* () {
// Uniform distribution (default)
const uniform = yield* Random.next
// Gaussian/Normal distribution
const gaussian = yield* Random.nextGaussian
// Exponential distribution
const exponential = yield* Random.nextExponential
// Custom distribution using transformation
const customDist = yield* pipe(
Effect.map(Random.next, n => Math.pow(n, 2)) // Quadratic distribution
)
return { uniform, gaussian, exponential, customDist }
})import { Effect, Random, Array as Arr } from "effect"
const deck = Array.from({ length: 52 }, (_, i) => {
const suits = ["♠", "♥", "♦", "♣"]
const ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
return `${ranks[i % 13]}${suits[Math.floor(i / 13)]}`
})
const cardOperations = Effect.gen(function* () {
// Shuffle array
const shuffled = yield* Random.shuffle(deck)
// Sample without replacement
const hand = yield* Random.sample(deck, 5)
// Sample with replacement
const randomCards = yield* Effect.all(
Array.from({ length: 10 }, () => Random.choice(deck))
)
return { shuffled, hand, randomCards }
})Building reproducible game states for testing and development:
import { Effect, Random, Chunk } from "effect"
interface GameEntity {
id: string
x: number
y: number
health: number
damage: number
type: "player" | "enemy" | "powerup"
}
// Helper for generating IDs
const randomId = pipe(
Effect.map(Random.nextIntBetween(100000, 999999), n => n.toString(36))
)
// Generate entities with specific distributions
const generateEntity = (type: GameEntity["type"]) =>
Effect.gen(function* () {
const id = yield* randomId
const x = yield* Random.nextIntBetween(0, 800)
const y = yield* Random.nextIntBetween(0, 600)
// Different stats based on type
const stats = yield* Effect.switch(type, {
player: () => Effect.succeed({ health: 100, damage: 25 }),
enemy: () => Effect.gen(function* () {
// Enemies have varied difficulty
const difficulty = yield* Random.nextIntBetween(1, 4)
return {
health: 20 * difficulty,
damage: 5 * difficulty
}
}),
powerup: () => Effect.succeed({ health: 0, damage: 0 })
})
return { id, x, y, type, ...stats }
})
// Generate complete game level
const generateLevel = (difficulty: number) =>
Effect.gen(function* () {
// More enemies at higher difficulty
const enemyCount = yield* Random.nextIntBetween(
3 + difficulty,
5 + difficulty * 2
)
// Fewer powerups at higher difficulty
const powerupCount = yield* Random.nextIntBetween(
Math.max(1, 5 - difficulty),
Math.max(2, 8 - difficulty)
)
const player = yield* generateEntity("player")
const enemies = yield* Effect.all(
Array.from({ length: enemyCount }, () => generateEntity("enemy"))
)
const powerups = yield* Effect.all(
Array.from({ length: powerupCount }, () => generateEntity("powerup"))
)
return {
player,
enemies,
powerups,
seed: yield* Random.getSeed
}
})
// Save and restore game state
const saveGameSeed = (level: any) => {
localStorage.setItem('gameSeed', JSON.stringify(level.seed))
}
const loadGameWithSeed = (seed: Random.Seed) =>
generateLevel(3).pipe(
Effect.withRandom(Random.makeDeterministicFromSeed(seed))
)Creating realistic test data for database seeding and testing:
import { Effect, Random, Option } from "effect"
// Weighted random selection
const weightedChoice = <T>(items: Array<[T, number]>) =>
Effect.gen(function* () {
const totalWeight = items.reduce((sum, [_, weight]) => sum + weight, 0)
const random = yield* Random.nextIntBetween(0, totalWeight)
let accumulator = 0
for (const [item, weight] of items) {
accumulator += weight
if (random < accumulator) {
return item
}
}
return items[0][0] // Fallback
})
// Realistic name generation
const firstNames = ["Alice", "Bob", "Carol", "David", "Emma", "Frank", "Grace", "Henry"]
const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller"]
const generateUser = Effect.gen(function* () {
const firstName = yield* Random.choice(firstNames)
const lastName = yield* Random.choice(lastNames)
// Weighted age distribution (more young users)
const ageGroup = yield* weightedChoice([
["18-25", 30],
["26-35", 25],
["36-45", 20],
["46-55", 15],
["56-65", 10]
])
const age = yield* Effect.switch(ageGroup, {
"18-25": () => Random.nextIntBetween(18, 26),
"26-35": () => Random.nextIntBetween(26, 36),
"36-45": () => Random.nextIntBetween(36, 46),
"46-55": () => Random.nextIntBetween(46, 56),
"56-65": () => Random.nextIntBetween(56, 66)
})
// Correlated data - older users more likely to be premium
const premiumProbability = Math.min(0.1 + (age - 18) * 0.01, 0.6)
const isPremium = yield* Effect.map(
Random.nextRange(0, 1),
n => n < premiumProbability
)
// Generate email with random provider
const emailProvider = yield* weightedChoice([
["gmail.com", 40],
["yahoo.com", 20],
["outlook.com", 25],
["company.com", 15]
])
return {
id: yield* Random.nextUUID,
firstName,
lastName,
email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@${emailProvider}`,
age,
isPremium,
createdAt: yield* generateDate(new Date(2020, 0, 1), new Date())
}
})
// Generate date in range
const generateDate = (start: Date, end: Date) =>
Effect.map(
Random.nextIntBetween(start.getTime(), end.getTime()),
timestamp => new Date(timestamp)
)
// Generate related data with constraints
const generateOrder = (userId: string) =>
Effect.gen(function* () {
const itemCount = yield* Random.nextIntBetween(1, 6)
const items = yield* Effect.all(
Array.from({ length: itemCount }, () =>
Effect.gen(function* () {
const product = yield* weightedChoice([
["laptop", 5],
["phone", 10],
["tablet", 7],
["headphones", 15],
["keyboard", 12],
["mouse", 12],
["monitor", 8],
["cable", 20]
])
const basePrice = {
laptop: 999,
phone: 699,
tablet: 499,
headphones: 199,
keyboard: 99,
mouse: 59,
monitor: 399,
cable: 19
}[product]
// Price variation
const priceMultiplier = yield* Random.nextRange(0.8, 1.2)
const quantity = yield* weightedChoice([
[1, 70],
[2, 20],
[3, 7],
[4, 3]
])
return {
product,
price: Math.round(basePrice * priceMultiplier * 100) / 100,
quantity
}
})
)
)
return {
id: yield* Random.nextUUID,
userId,
items,
total: items.reduce((sum, item) => sum + item.price * item.quantity, 0),
createdAt: yield* generateDate(new Date(2023, 0, 1), new Date())
}
})Running probabilistic simulations with controlled randomness:
import { Effect, Random, Array as Arr, Number as N } from "effect"
// Stock price simulation using Geometric Brownian Motion
interface StockSimulationParams {
initialPrice: number
drift: number // Annual drift (μ)
volatility: number // Annual volatility (σ)
timeSteps: number // Number of time steps
dt: number // Time increment (years)
}
const simulateStockPrice = (params: StockSimulationParams) =>
Effect.gen(function* () {
const { initialPrice, drift, volatility, timeSteps, dt } = params
const prices: number[] = [initialPrice]
for (let i = 0; i < timeSteps; i++) {
const z = yield* Random.nextGaussian // Standard normal
const currentPrice = prices[i]
// GBM formula: S(t+dt) = S(t) * exp((μ - σ²/2)dt + σ√dt * Z)
const nextPrice = currentPrice * Math.exp(
(drift - 0.5 * volatility * volatility) * dt +
volatility * Math.sqrt(dt) * z
)
prices.push(nextPrice)
}
return prices
})
// Monte Carlo option pricing
interface OptionParams {
spotPrice: number
strikePrice: number
riskFreeRate: number
volatility: number
timeToExpiry: number
optionType: "call" | "put"
}
const monteCarloOptionPrice = (params: OptionParams, simulations: number = 10000) =>
Effect.gen(function* () {
const { spotPrice, strikePrice, riskFreeRate, volatility, timeToExpiry, optionType } = params
// Run multiple simulations
const payoffs = yield* Effect.all(
Array.from({ length: simulations }, () =>
Effect.gen(function* () {
// Generate final price using GBM
const z = yield* Random.nextGaussian
const finalPrice = spotPrice * Math.exp(
(riskFreeRate - 0.5 * volatility * volatility) * timeToExpiry +
volatility * Math.sqrt(timeToExpiry) * z
)
// Calculate payoff
if (optionType === "call") {
return Math.max(0, finalPrice - strikePrice)
} else {
return Math.max(0, strikePrice - finalPrice)
}
})
),
{ concurrency: "unbounded" }
)
// Calculate option price
const averagePayoff = Arr.reduce(payoffs, 0, N.sum) / simulations
const discountedPrice = averagePayoff * Math.exp(-riskFreeRate * timeToExpiry)
// Calculate standard error
const squaredDiffs = payoffs.map(p => Math.pow(p - averagePayoff, 2))
const variance = Arr.reduce(squaredDiffs, 0, N.sum) / (simulations - 1)
const standardError = Math.sqrt(variance) / Math.sqrt(simulations)
return {
price: discountedPrice,
standardError,
confidenceInterval: {
lower: discountedPrice - 1.96 * standardError,
upper: discountedPrice + 1.96 * standardError
}
}
})
// Risk analysis with multiple scenarios
const portfolioRiskAnalysis = (
portfolio: Array<{ symbol: string; shares: number; price: number }>,
correlationMatrix: number[][]
) =>
Effect.gen(function* () {
const scenarios = 1000
const timeHorizon = 252 // Trading days
const returns = yield* Effect.all(
Array.from({ length: scenarios }, () =>
Effect.gen(function* () {
// Generate correlated returns using Cholesky decomposition
const uncorrelated = yield* Effect.all(
portfolio.map(() => Random.nextGaussian)
)
// Apply correlation (simplified - real implementation would use Cholesky)
const correlated = portfolio.map((_, i) =>
uncorrelated.reduce((sum, u, j) =>
sum + u * (correlationMatrix[i]?.[j] ?? 0), 0
)
)
// Calculate portfolio return
const totalValue = portfolio.reduce((sum, stock) =>
sum + stock.shares * stock.price, 0
)
const scenarioReturn = portfolio.reduce((sum, stock, i) => {
const stockReturn = correlated[i] * 0.2 / Math.sqrt(252) // 20% annual vol
const newPrice = stock.price * (1 + stockReturn)
return sum + stock.shares * newPrice
}, 0) / totalValue - 1
return scenarioReturn
})
)
)
// Calculate VaR and CVaR
const sortedReturns = [...returns].sort((a, b) => a - b)
const varIndex = Math.floor(scenarios * 0.05) // 5% VaR
const var5 = sortedReturns[varIndex]
const cvar5 = sortedReturns.slice(0, varIndex).reduce((sum, r) => sum + r, 0) / varIndex
return {
expectedReturn: returns.reduce((sum, r) => sum + r, 0) / scenarios,
volatility: Math.sqrt(
returns.reduce((sum, r) => sum + r * r, 0) / scenarios -
Math.pow(returns.reduce((sum, r) => sum + r, 0) / scenarios, 2)
),
var5,
cvar5,
worstCase: sortedReturns[0],
bestCase: sortedReturns[scenarios - 1]
}
})Effect's Random module provides sophisticated seed management for complex scenarios:
import { Effect, Random, Ref } from "effect"
// Save and restore random state
const randomWithCheckpoint = Effect.gen(function* () {
// Get current seed
const initialSeed = yield* Random.getSeed
// Do some random operations
const values1 = yield* Effect.all([
Random.nextIntBetween(1, 100),
Random.nextIntBetween(1, 100)
])
// Save checkpoint
const checkpoint = yield* Random.getSeed
// More operations
const values2 = yield* Effect.all([
Random.nextIntBetween(1, 100),
Random.nextIntBetween(1, 100)
])
// Restore to checkpoint and replay
yield* Random.setSeed(checkpoint)
const values2Replay = yield* Effect.all([
Random.nextIntBetween(1, 100),
Random.nextIntBetween(1, 100)
])
return {
values1,
values2,
values2Replay, // Will be identical to values2
canRestore: JSON.stringify(values2) === JSON.stringify(values2Replay)
}
})
// Branching random streams for parallel simulations
const parallelSimulations = Effect.gen(function* () {
const baseSeed = yield* Random.getSeed
// Create independent random streams
const results = yield* Effect.all(
Array.from({ length: 4 }, (_, i) =>
Effect.gen(function* () {
// Each branch gets its own seed derived from base
const branchSeed = Random.makeSeedFromNumber(baseSeed.seed + i)
return yield* Effect.provide(
simulateScenario(i),
Random.layerDeterministicFromSeed(branchSeed)
)
})
),
{ concurrency: 4 }
)
return results
})
// Custom random service for specific domains
class GameRandom extends Effect.Tag("GameRandom")<
GameRandom,
{
readonly rollDice: (sides: number) => Effect.Effect<number>
readonly flipCoin: Effect.Effect<"heads" | "tails">
readonly drawCard: Effect.Effect<{ suit: string; rank: string }>
}
>() {}
const GameRandomLive = Layer.effect(
GameRandom,
Effect.gen(function* () {
const random = yield* Random.Random
return GameRandom.of({
rollDice: (sides: number) =>
random.nextIntBetween(1, sides + 1),
flipCoin: random.nextBoolean.pipe(
Effect.map(b => b ? "heads" : "tails")
),
drawCard: Effect.gen(function* () {
const suits = ["♠", "♥", "♦", "♣"]
const ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
const suit = yield* random.choice(suits)
const rank = yield* random.choice(ranks)
return { suit, rank }
})
})
})
)Creating custom probability distributions:
import { Effect, Random, pipe } from "effect"
// Beta distribution using rejection sampling
const nextBeta = (alpha: number, beta: number): Effect.Effect<number, never, Random.Random> =>
Effect.gen(function* () {
while (true) {
const u1 = yield* Random.next
const u2 = yield* Random.next
const x = Math.pow(u1, 1 / alpha)
const y = Math.pow(u2, 1 / beta)
if (x + y <= 1) {
return x / (x + y)
}
}
})
// Poisson distribution
const nextPoisson = (lambda: number): Effect.Effect<number, never, Random.Random> =>
Effect.gen(function* () {
const L = Math.exp(-lambda)
let k = 0
let p = 1
do {
k++
p *= yield* Random.next
} while (p > L)
return k - 1
})
// Custom discrete distribution
const createDiscreteDistribution = <T>(
probabilities: Map<T, number>
): Effect.Effect<T, never, Random.Random> => {
// Normalize probabilities
const total = Array.from(probabilities.values()).reduce((a, b) => a + b, 0)
const normalized = new Map(
Array.from(probabilities).map(([value, prob]) => [value, prob / total])
)
// Create cumulative distribution
const cumulative: Array<[T, number]> = []
let sum = 0
for (const [value, prob] of normalized) {
sum += prob
cumulative.push([value, sum])
}
return Effect.gen(function* () {
const r = yield* Random.next
for (const [value, cum] of cumulative) {
if (r <= cum) return value
}
return cumulative[cumulative.length - 1][0]
})
}
// Markov chain state transitions
interface MarkovChain<S> {
transitions: Map<S, Map<S, number>>
}
const markovStep = <S>(
chain: MarkovChain<S>,
currentState: S
): Effect.Effect<S, never, Random.Random> => {
const transitions = chain.transitions.get(currentState)
if (!transitions) {
return Effect.succeed(currentState)
}
return createDiscreteDistribution(transitions)
}
// Example: Weather simulation
type Weather = "sunny" | "cloudy" | "rainy"
const weatherChain: MarkovChain<Weather> = {
transitions: new Map([
["sunny", new Map([
["sunny", 0.7],
["cloudy", 0.2],
["rainy", 0.1]
])],
["cloudy", new Map([
["sunny", 0.3],
["cloudy", 0.4],
["rainy", 0.3]
])],
["rainy", new Map([
["sunny", 0.2],
["cloudy", 0.3],
["rainy", 0.5]
])]
])
}
const simulateWeather = (days: number, initial: Weather = "sunny") =>
Effect.gen(function* () {
const states: Weather[] = [initial]
let current = initial
for (let i = 1; i < days; i++) {
current = yield* markovStep(weatherChain, current)
states.push(current)
}
return states
})Optimizing random generation for high-performance scenarios:
import { Effect, Random, Chunk } from "effect"
// Batch random generation for better performance
const generateBatch = <T>(
generator: Effect.Effect<T, never, Random.Random>,
size: number
): Effect.Effect<Chunk.Chunk<T>, never, Random.Random> =>
Effect.gen(function* () {
// Pre-allocate for performance
const results = new Array(size)
for (let i = 0; i < size; i++) {
results[i] = yield* generator
}
return Chunk.fromIterable(results)
})
// Cached random values for repeated sampling
const createRandomCache = <T>(
generator: Effect.Effect<T, never, Random.Random>,
cacheSize: number
) =>
Effect.gen(function* () {
const cache = yield* generateBatch(generator, cacheSize)
const index = yield* Ref.make(0)
const next = Effect.gen(function* () {
const i = yield* Ref.getAndUpdate(index, i => (i + 1) % cacheSize)
return Chunk.unsafeGet(cache, i)
})
return { next, refill: Effect.unit }
})
// Parallel random generation with work stealing
const parallelMonteCarlo = <T>(
task: Effect.Effect<T, never, Random.Random>,
iterations: number,
parallelism: number = 4
) =>
Effect.gen(function* () {
const chunkSize = Math.ceil(iterations / parallelism)
// Create independent random streams for each worker
const results = yield* Effect.all(
Array.from({ length: parallelism }, (_, i) =>
Effect.gen(function* () {
const seed = yield* Random.getSeed
const workerSeed = Random.makeSeedFromNumber(seed.seed + i)
const workerIterations = i === parallelism - 1
? iterations - (chunkSize * (parallelism - 1))
: chunkSize
return yield* Effect.all(
Array.from({ length: workerIterations }, () => task),
{ concurrency: "unbounded" }
).pipe(
Effect.withRandom(Random.makeDeterministicFromSeed(workerSeed))
)
})
),
{ concurrency: parallelism }
)
return results.flat()
})Create reusable factories for generating test data:
import { Effect, Random, Schema } from "effect"
// Generic factory builder
class Factory<T> {
constructor(
private readonly generator: Effect.Effect<T, never, Random.Random>
) {}
build(): Effect.Effect<T, never, Random.Random> {
return this.generator
}
buildMany(count: number): Effect.Effect<T[], never, Random.Random> {
return Effect.all(
Array.from({ length: count }, () => this.generator)
)
}
withOverrides(overrides: Partial<T>): Factory<T> {
return new Factory(
this.generator.pipe(
Effect.map(value => ({ ...value, ...overrides }))
)
)
}
map<U>(f: (value: T) => U): Factory<U> {
return new Factory(pipe(Effect.map(this.generator, f)))
}
}
// Schema-based random generation
const schemaToRandom = <A, I>(schema: Schema.Schema<A, I>): Effect.Effect<A, never, Random.Random> => {
// Implementation would inspect schema type and generate appropriate random values
// This is a simplified example
return Random.nextIntBetween(0, 100) as any
}
// Define factories
const UserFactory = new Factory(
Effect.gen(function* () {
const id = yield* Random.nextUUID
const age = yield* Random.nextIntBetween(18, 80)
const name = yield* Random.choice(["Alice", "Bob", "Charlie", "Diana"])
const email = `${name.toLowerCase()}${yield* Random.nextIntBetween(1, 1000)}@example.com`
return {
id,
name,
email,
age,
createdAt: new Date()
}
})
)
const OrderFactory = new Factory(
Effect.gen(function* () {
const user = yield* UserFactory.build()
const itemCount = yield* Random.nextIntBetween(1, 5)
const items = yield* Effect.all(
Array.from({ length: itemCount }, () =>
Effect.gen(function* () {
const products = ["Laptop", "Phone", "Tablet", "Watch", "Headphones"]
const product = yield* Random.choice(products)
const quantity = yield* Random.nextIntBetween(1, 3)
const price = yield* Random.nextIntBetween(50, 2000)
return { product, quantity, price }
})
)
)
return {
id: yield* Random.nextUUID,
userId: user.id,
items,
total: items.reduce((sum, item) => sum + item.price * item.quantity, 0),
status: yield* Random.choice(["pending", "processing", "shipped", "delivered"] as const)
}
})
)
// Usage in tests
const testScenario = Effect.gen(function* () {
// Generate test data
const users = yield* UserFactory.buildMany(10)
const premiumUsers = yield* UserFactory
.withOverrides({ premium: true })
.buildMany(5)
const orders = yield* Effect.all(
users.map(user =>
OrderFactory
.withOverrides({ userId: user.id })
.build()
)
)
return { users, premiumUsers, orders }
})Utilities for making tests with randomness reliable:
import { Effect, Random, TestClock, TestContext } from "effect"
// Test helper for deterministic random
const withTestRandom = <A, E, R>(
effect: Effect.Effect<A, E, R>,
seed: string = "test"
): Effect.Effect<A, E, Exclude<R, Random.Random>> =>
effect.pipe(
Effect.withRandom(Random.makeDeterministic(seed))
)
// Snapshot testing for random generation
const snapshotRandom = <A>(
name: string,
generator: Effect.Effect<A, never, Random.Random>,
options: { seed?: string; iterations?: number } = {}
) => {
const { seed = "snapshot", iterations = 1 } = options
return Effect.gen(function* () {
const results = yield* Effect.all(
Array.from({ length: iterations }, () => generator)
).pipe(
Effect.withRandom(Random.makeDeterministic(seed))
)
// In a real implementation, this would save/compare snapshots
console.log(`Snapshot '${name}':`, JSON.stringify(results, null, 2))
return results
})
}
// Property-based testing integration
const forAll = <A>(
generator: Effect.Effect<A, never, Random.Random>,
property: (value: A) => boolean,
options: { runs?: number; seed?: string } = {}
) => {
const { runs = 100, seed } = options
return Effect.gen(function* () {
const baseSeed = seed
? Random.makeDeterministic(seed)
: yield* Random.Random
for (let i = 0; i < runs; i++) {
const testSeed = Random.makeSeedFromNumber(baseSeed.getSeed.seed + i)
const value = yield* generator.pipe(
Effect.withRandom(Random.makeDeterministicFromSeed(testSeed))
)
if (!property(value)) {
return Effect.fail({
message: "Property failed",
value,
seed: testSeed,
run: i
})
}
}
return Effect.succeed({ passed: runs })
})
}
// Regression test helper
const regressionTest = <A>(
name: string,
scenario: Effect.Effect<A, never, Random.Random>,
expectedChecksum: string
) =>
Effect.gen(function* () {
const result = yield* scenario.pipe(
Effect.withRandom(Random.makeDeterministic(name))
)
// Simple checksum for demonstration
const checksum = JSON.stringify(result).split('').reduce(
(acc, char) => acc + char.charCodeAt(0), 0
).toString(36)
if (checksum !== expectedChecksum) {
return Effect.fail({
message: `Regression detected in '${name}'`,
expected: expectedChecksum,
actual: checksum
})
}
return Effect.succeed(result)
})Using Random with fast-check for property-based testing:
import { Effect, Random } from "effect"
import * as fc from "fast-check"
// Effect arbitrary from Random generator
const effectArbitrary = <A>(
generator: Effect.Effect<A, never, Random.Random>
): fc.Arbitrary<A> =>
fc.integer().map(seed =>
Effect.runSync(
generator.pipe(
Effect.withRandom(Random.makeSeedFromNumber(seed))
)
)
)
// Custom arbitraries using Effect Random
const userArbitrary = effectArbitrary(
Effect.gen(function* () {
const id = yield* Random.nextIntBetween(1, 10000)
const name = yield* Random.choice(["Alice", "Bob", "Charlie"])
const age = yield* Random.nextIntBetween(0, 120)
return { id, name, age }
})
)
// Property tests with Effect using simple pipe
const propertyTest = (name: string, property: fc.IProperty<unknown>) =>
Effect.gen(function* () {
const result = yield* Effect.sync(() => fc.check(property))
if (result.failed) {
return yield* Effect.fail(`Property '${name}' failed: ${result.counterexample}`)
}
return yield* Effect.unit
})
// Example property test
const userProperties = Effect.gen(function* () {
yield* propertyTest(
"users have valid ages",
fc.property(userArbitrary, (user) =>
user.age >= 0 && user.age <= 120
)
)
yield* propertyTest(
"user IDs are positive",
fc.property(userArbitrary, (user) =>
user.id > 0
)
)
})
// Stateful property testing
interface Command {
readonly _tag: string
}
class AddUserCommand implements Command {
readonly _tag = "AddUser"
constructor(readonly user: { name: string; age: number }) {}
}
class RemoveUserCommand implements Command {
readonly _tag = "RemoveUser"
constructor(readonly id: number) {}
}
const commandArbitrary = effectArbitrary(
Effect.gen(function* () {
const type = yield* Random.nextBoolean
if (type) {
const name = yield* Random.choice(["Alice", "Bob", "Charlie"])
const age = yield* Random.nextIntBetween(18, 80)
return new AddUserCommand({ name, age })
} else {
const id = yield* Random.nextIntBetween(1, 100)
return new RemoveUserCommand(id)
}
})
)
// Model-based testing
const modelBasedTest = Effect.gen(function* () {
const commands = yield* Effect.all(
Array.from({ length: 20 }, () => commandArbitrary)
)
// Run commands against both model and real system
const modelResult = runCommandsOnModel(commands)
const systemResult = yield* runCommandsOnSystem(commands)
// Compare results
if (JSON.stringify(modelResult) !== JSON.stringify(systemResult)) {
return Effect.fail("Model and system diverged")
}
return Effect.succeed("Model-based test passed")
})Comprehensive testing strategies for random-dependent code:
import { Effect, Random, Fiber, TestClock } from "effect"
// Golden master testing with Effect.gen + yield* for complex logic
const goldenMasterTest = <A>(
name: string,
computation: Effect.Effect<A, never, Random.Random>
) => {
const goldenPath = `./golden/${name}.json`
return Effect.gen(function* () {
const result = yield* computation.pipe(
Effect.withRandom(Random.makeDeterministic(name))
)
try {
// Compare with golden file
const golden = JSON.parse(fs.readFileSync(goldenPath, 'utf8'))
if (JSON.stringify(result) !== JSON.stringify(golden)) {
return yield* Effect.fail({
message: "Output differs from golden master",
expected: golden,
actual: result
})
}
} catch {
// Create golden file if it doesn't exist
fs.writeFileSync(goldenPath, JSON.stringify(result, null, 2))
console.log(`Created golden master: ${goldenPath}`)
}
return result
})
}
// Fuzzing helper with Effect.gen + yield* for iterative testing
const fuzz = <A, E>(
program: (input: A) => Effect.Effect<unknown, E>,
generator: Effect.Effect<A, never, Random.Random>,
options: { timeout: number; runs: number } = { timeout: 5000, runs: 1000 }
) =>
Effect.gen(function* () {
const errors: Array<{ input: A; error: E }> = []
for (let i = 0; i < options.runs; i++) {
const input = yield* generator
const result = yield* program(input).pipe(
Effect.timeout(options.timeout),
Effect.either
)
if (result._tag === "Left") {
errors.push({ input, error: result.left })
}
}
return {
runs: options.runs,
errors,
errorRate: errors.length / options.runs
}
})
// Chaos testing
const chaosTest = <A, E, R>(
program: Effect.Effect<A, E, R>,
chaos: {
failureRate: number
delayRange: [number, number]
resourceFailureRate: number
}
) =>
Effect.gen(function* () {
// Inject random failures
const shouldFail = yield* Effect.map(
Random.nextRange(0, 1),
n => n < chaos.failureRate
)
if (shouldFail) {
return yield* Effect.fail("Chaos: Random failure injected" as E)
}
// Inject random delays
const delay = yield* Random.nextIntBetween(...chaos.delayRange)
yield* TestClock.sleep(delay)
// Run the actual program
return yield* program
})
// Statistical validation
const validateDistribution = <T>(
generator: Effect.Effect<T, never, Random.Random>,
samples: number,
validator: (samples: T[]) => { valid: boolean; reason?: string }
) =>
Effect.gen(function* () {
const data = yield* Effect.all(
Array.from({ length: samples }, () => generator)
)
const result = validator(data)
if (!result.valid) {
return Effect.fail(`Distribution validation failed: ${result.reason}`)
}
return Effect.succeed({
samples,
valid: true
})
})
// Example: Validate normal distribution with pipe for utility
const validateNormalDistribution = pipe(
Random.nextGaussian,
(generator) => validateDistribution(
generator,
10000,
(samples) => {
const mean = samples.reduce((a, b) => a + b, 0) / samples.length
const variance = samples.reduce((sum, x) => sum + Math.pow(x - mean, 2), 0) / samples.length
const stdDev = Math.sqrt(variance)
// Check if mean is close to 0 and std dev is close to 1
const meanOk = Math.abs(mean) < 0.05
const stdDevOk = Math.abs(stdDev - 1) < 0.05
return {
valid: meanOk && stdDevOk,
reason: `Mean: ${mean.toFixed(3)}, StdDev: ${stdDev.toFixed(3)}`
}
}
)
)Random provides deterministic, composable random generation for building reproducible, testable applications with controlled randomness.
Key benefits:
- Deterministic Testing: Reproducible random generation with seed control for reliable tests
- Effect Integration: Seamlessly works with Effect's service pattern and composition
- Rich Distributions: Built-in support for various probability distributions beyond uniform random
Random is essential when you need controlled randomness for testing, simulations, game development, or any scenario requiring reproducible pseudo-random generation within the Effect ecosystem.