Scala - Random Number Generation

• Scala provides multiple approaches to random number generation through `scala.util.Random`, Java's `java.util.Random`, and `java.security.SecureRandom` for cryptographically secure operations

Key Insights

• Scala provides multiple approaches to random number generation through scala.util.Random, Java’s java.util.Random, and java.security.SecureRandom for cryptographically secure operations • The Random class offers methods for generating primitives, collections, and custom distributions, with thread-safe alternatives available through ThreadLocalRandom • Understanding seeding, reproducibility, and performance characteristics is critical for choosing the right random number generation strategy in production systems

Basic Random Number Generation

Scala’s scala.util.Random wraps Java’s random number generator with a more idiomatic Scala interface. The simplest approach creates a default instance:

import scala.util.Random

val random = new Random()

// Generate random integers
val randomInt = random.nextInt()           // Any Int value
val boundedInt = random.nextInt(100)       // 0 to 99
val rangeInt = random.between(10, 50)      // 10 to 49

// Generate random doubles
val randomDouble = random.nextDouble()     // 0.0 to 1.0 (exclusive)
val scaledDouble = random.nextDouble() * 100  // 0.0 to 100.0

// Generate random booleans
val randomBool = random.nextBoolean()

// Generate random longs
val randomLong = random.nextLong()
val boundedLong = random.between(1000L, 5000L)

The between method, introduced in Scala 2.13, provides inclusive lower bounds and exclusive upper bounds, making range-based generation more intuitive than the traditional modulo approach.

Seeding for Reproducibility

Seeding enables reproducible random sequences, essential for testing, debugging, and scenarios requiring deterministic behavior:

import scala.util.Random

// Create two Random instances with the same seed
val random1 = new Random(42)
val random2 = new Random(42)

// Both generate identical sequences
println(random1.nextInt(100))  // Same value
println(random2.nextInt(100))  // Same value

// Practical testing example
class DiceRoller(random: Random) {
  def roll(): Int = random.between(1, 7)
}

// Deterministic test
val testRandom = new Random(12345)
val roller = new DiceRoller(testRandom)
assert(roller.roll() == 5)  // Predictable for this seed

This approach separates randomness from business logic, making code testable without mocking frameworks. Pass different Random instances for testing versus production.

Generating Random Collections

Scala’s Random provides methods specifically designed for collections:

import scala.util.Random

val random = new Random()

// Shuffle a sequence
val numbers = (1 to 10).toList
val shuffled = random.shuffle(numbers)

// Select random elements
val colors = List("red", "blue", "green", "yellow", "purple")
val randomColor = colors(random.nextInt(colors.length))

// More idiomatic with shuffle and head
val anotherColor = random.shuffle(colors).head

// Generate random sequences
def randomString(length: Int): String = {
  val chars = ('a' to 'z') ++ ('A' to 'Z') ++ ('0' to '9')
  (1 to length).map(_ => chars(random.nextInt(chars.length))).mkString
}

println(randomString(10))  // e.g., "aB3xK9mPq2"

// Generate random collection with specific size
val randomInts = List.fill(5)(random.nextInt(100))

The shuffle method uses the Fisher-Yates algorithm, providing uniform randomness across all permutations with O(n) complexity.

Thread-Safe Random Number Generation

In concurrent applications, sharing a single Random instance creates contention. Use ThreadLocalRandom for better performance:

import java.util.concurrent.ThreadLocalRandom
import scala.concurrent.{Future, ExecutionContext}
import scala.concurrent.ExecutionContext.Implicits.global

// Thread-safe random generation
def generateRandomId(): String = {
  val random = ThreadLocalRandom.current()
  val timestamp = System.currentTimeMillis()
  val randomPart = random.nextInt(100000)
  f"$timestamp%d-$randomPart%05d"
}

// Use in concurrent context
val futures = (1 to 100).map { _ =>
  Future {
    generateRandomId()
  }
}

// Each thread gets its own Random instance automatically

ThreadLocalRandom eliminates synchronization overhead while maintaining statistical quality. Never share instances across threads—always call ThreadLocalRandom.current() within each thread.

Cryptographically Secure Random Numbers

For security-sensitive operations like token generation, API keys, or cryptographic operations, use SecureRandom:

import java.security.SecureRandom
import java.util.Base64

object SecureTokenGenerator {
  private val secureRandom = new SecureRandom()
  
  def generateToken(bytes: Int = 32): String = {
    val tokenBytes = new Array[Byte](bytes)
    secureRandom.nextBytes(tokenBytes)
    Base64.getUrlEncoder.withoutPadding().encodeToString(tokenBytes)
  }
  
  def generateNumericCode(length: Int = 6): String = {
    (1 to length).map(_ => secureRandom.nextInt(10)).mkString
  }
  
  // Generate secure random string with specific character set
  def generateSecureString(length: Int, chars: String): String = {
    (1 to length).map { _ =>
      chars.charAt(secureRandom.nextInt(chars.length))
    }.mkString
  }
}

// Usage
val apiKey = SecureTokenGenerator.generateToken()
val verificationCode = SecureTokenGenerator.generateNumericCode()
val password = SecureTokenGenerator.generateSecureString(
  16, 
  "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%"
)

SecureRandom uses platform-specific entropy sources (like /dev/urandom on Unix systems) and is significantly slower than Random. Use it only when cryptographic strength is required.

Custom Distributions

While Random provides uniform distribution, many applications require custom distributions:

import scala.util.Random

class CustomDistributions(random: Random = new Random()) {
  
  // Gaussian (normal) distribution
  def gaussian(mean: Double, stdDev: Double): Double = {
    random.nextGaussian() * stdDev + mean
  }
  
  // Exponential distribution
  def exponential(lambda: Double): Double = {
    -Math.log(1.0 - random.nextDouble()) / lambda
  }
  
  // Weighted random selection
  def weighted[T](items: Seq[(T, Double)]): T = {
    val totalWeight = items.map(_._2).sum
    val randomValue = random.nextDouble() * totalWeight
    
    items.foldLeft((0.0, Option.empty[T])) {
      case ((accumulated, None), (item, weight)) =>
        val newAccumulated = accumulated + weight
        if (randomValue <= newAccumulated) (newAccumulated, Some(item))
        else (newAccumulated, None)
      case (result, _) => result
    }._2.get
  }
}

// Usage examples
val distributions = new CustomDistributions()

// Generate heights with mean 170cm, std dev 10cm
val heights = List.fill(1000)(distributions.gaussian(170, 10))

// Simulate time between events (lambda = 0.5)
val intervals = List.fill(100)(distributions.exponential(0.5))

// Weighted random selection
val outcomes = Seq(
  ("common", 70.0),
  ("uncommon", 20.0),
  ("rare", 9.0),
  ("legendary", 1.0)
)
val result = distributions.weighted(outcomes)

The weighted selection algorithm ensures each item’s probability matches its weight proportion, useful for game mechanics, A/B testing, and simulation scenarios.

Performance Considerations and Best Practices

Understanding performance characteristics helps optimize random number generation:

import scala.util.Random
import java.util.concurrent.ThreadLocalRandom

object RandomBenchmark {
  
  // Avoid creating new Random instances repeatedly
  // Bad: Creates new instance each call
  def badApproach(): Int = {
    new Random().nextInt(100)  // Slow, uses current time as seed
  }
  
  // Good: Reuse instance
  private val random = new Random()
  def goodApproach(): Int = {
    random.nextInt(100)
  }
  
  // Best for concurrent scenarios
  def concurrentApproach(): Int = {
    ThreadLocalRandom.current().nextInt(100)
  }
  
  // Efficient batch generation
  def generateBatch(size: Int): Array[Int] = {
    val result = new Array[Int](size)
    var i = 0
    while (i < size) {
      result(i) = random.nextInt(100)
      i += 1
    }
    result
  }
}

Key performance guidelines: reuse Random instances in single-threaded contexts, use ThreadLocalRandom for concurrent scenarios, avoid SecureRandom unless security is required, and prefer batch generation over repeated individual calls.

Practical Example: ID Generation System

Combining these concepts into a production-ready ID generation system:

import java.util.concurrent.ThreadLocalRandom
import java.security.SecureRandom
import java.util.Base64

trait IdGenerator {
  def generate(): String
}

class NumericIdGenerator extends IdGenerator {
  def generate(): String = {
    val random = ThreadLocalRandom.current()
    val timestamp = System.currentTimeMillis()
    val randomPart = random.nextInt(1000000)
    f"$timestamp%d$randomPart%06d"
  }
}

class UuidStyleGenerator extends IdGenerator {
  def generate(): String = {
    val random = ThreadLocalRandom.current()
    val uuid = new Array[Byte](16)
    random.nextBytes(uuid)
    
    // Set version and variant bits
    uuid(6) = ((uuid(6) & 0x0f) | 0x40).toByte
    uuid(8) = ((uuid(8) & 0x3f) | 0x80).toByte
    
    Base64.getUrlEncoder.withoutPadding().encodeToString(uuid)
  }
}

class SecureIdGenerator extends IdGenerator {
  private val secureRandom = new SecureRandom()
  
  def generate(): String = {
    val bytes = new Array[Byte](24)
    secureRandom.nextBytes(bytes)
    Base64.getUrlEncoder.withoutPadding().encodeToString(bytes)
  }
}

// Factory pattern for different use cases
object IdGeneratorFactory {
  def forUserIds: IdGenerator = new SecureIdGenerator()
  def forRequestIds: IdGenerator = new NumericIdGenerator()
  def forSessionIds: IdGenerator = new UuidStyleGenerator()
}

This design separates concerns, allows testing with custom implementations, and provides appropriate security levels for different use cases.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.