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.