Streams
Remarks#
Streams are lazily-evaluated, meaning they can be used to implement generators, which will provide or ‘generate’ a new item of the specified type on-demand, rather than before the fact. This ensures only the computations necessary are done.
Using a Stream to Generate a Random Sequence
genRandom
creates a stream of random numbers that has a one in four chance of terminating each time it’s called.
def genRandom: Stream[String] = {
val random = scala.util.Random.nextFloat()
println(s"Random value is: $random")
if (random < 0.25) {
Stream.empty[String]
} else {
("%.3f : A random number" format random) #:: genRandom
}
}
lazy val randos = genRandom // getRandom is lazily evaluated as randos is iterated through
for {
x <- randos
} println(x) // The number of times this prints is effectively randomized.
Note the #::
construct, which lazily recurses: because it is prepending the current random number to a stream, it does not evaluate the remainder of the stream until it is iterated through.
Infinite Streams via Recursion
Streams can be built that reference themselves and thus become infinitely recursive.
// factorial
val fact: Stream[BigInt] = 1 #:: fact.zipWithIndex.map{case (p,x)=>p*(x+1)}
fact.take(10) // (1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880)
fact(24) // 620448401733239439360000
// the Fibonacci series
val fib: Stream[BigInt] = 0 #:: fib.scan(1:BigInt)(_+_)
fib.take(10) // (0, 1, 1, 2, 3, 5, 8, 13, 21, 34)
fib(124) // 36726740705505779255899443
// random Ints between 10 and 99 (inclusive)
def rndInt: Stream[Int] = (util.Random.nextInt(90)+10) #:: rndInt
rndInt.take(10) // (20, 95, 14, 44, 42, 78, 85, 24, 99, 85)
In this context the difference between Var, Val, and Def is interesting. As a def
each element is recalculated every time it is referenced. As a val
each element is retained and reused after it’s been calculated. This can be demonstrated by creating a side-effect with each calculation.
// def with extra output per calculation
def fact: Stream[Int] = 1 #:: fact.zipWithIndex.map{case (p,x)=>print("!");p*(x+1)}
fact(5) // !!!!!!!!!!!!!!! 120
fact(4) // !!!!!!!!!! 24
fact(7) // !!!!!!!!!!!!!!!!!!!!!!!!!!!! 5040
// now as val
val fact: Stream[Int] = 1 #:: fact.zipWithIndex.map{case (p,x)=>print("!");p*(x+1)}
fact(5) // !!!!! 120
fact(4) // 24
fact(7) // !! 5040
This also explains why the random number Stream
doesn’t work as a val
.
val rndInt: Stream[Int] = (util.Random.nextInt(90)+10) #:: rndInt
rndInt.take(5) // (79, 79, 79, 79, 79)
Infinite self-referent stream
// Generate stream that references itself in its evaluation
lazy val primes: Stream[Int] =
2 #:: Stream.from(3, 2)
.filter { i => primes.takeWhile(p => p * p <= i).forall(i % _ != 0) }
.takeWhile(_ > 0) // prevent overflowing
// Get list of 10 primes
assert(primes.take(10).toList == List(2, 3, 5, 7, 11, 13, 17, 19, 23, 29))
// Previously calculated values were memoized, as shown by toString
assert(primes.toString == "Stream(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, ?)")