Skip to content
This repository was archived by the owner on Jul 11, 2025. It is now read-only.

Latest commit

 

History

History
1383 lines (1130 loc) · 37.4 KB

File metadata and controls

1383 lines (1130 loc) · 37.4 KB

Random: A Real-World Guide

Table of Contents

  1. Introduction & Core Concepts
  2. Basic Usage Patterns
  3. Real-World Examples
  4. Advanced Features Deep Dive
  5. Practical Patterns & Best Practices
  6. Integration Examples

Introduction & Core Concepts

The Problem Random Solves

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.

The Random Solution

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)
})

Key Concepts

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.

Basic Usage Patterns

Basic Random Operations

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"))
  )
)

Random Distributions

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 }
})

Shuffling and Sampling

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 }
})

Real-World Examples

Example 1: Game State Generation

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))
  )

Example 2: Test Data Generation

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())
    }
  })

Example 3: Simulation and Monte Carlo

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]
    }
  })

Advanced Features Deep Dive

Seed Management and Persistence

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 }
      })
    })
  })
)

Custom Distributions

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
  })

Performance Optimization

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()
  })

Practical Patterns & Best Practices

Pattern 1: Test Data Factories

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 }
})

Pattern 2: Deterministic Testing Helpers

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)
  })

Integration Examples

Integration with Property-Based Testing

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")
})

Testing Strategies

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)}`
      }
    }
  )
)

Conclusion

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.