Ответ 1
Scalac и REPL хороши с этим кодом (с использованием val), пока переменная является полем класса, а не локальной переменной. Вы можете сделать переменную ленивой, чтобы удовлетворить Scala Kata, но вы, как правило, не хотели бы использовать def таким образом (то есть def Stream в терминах самого себя) в реальной программе. Если это так, новый поток создается каждый раз при вызове метода, поэтому результаты предыдущих вычислений (которые сохраняются в потоке) никогда не могут быть повторно использованы. Если вы используете много значений из такого потока, производительность будет ужасной, и в итоге у вас не хватит памяти.
Эта программа демонстрирует проблему с использованием def таким образом:
// Show the difference between the use of val and def with Streams.
object StreamTest extends App {
def sum( p:(Int,Int) ) = { println( "sum " + p ); p._1 + p._2 }
val fibs1: Stream[Int] = 0 #:: 1 #:: ( fibs1 zip fibs1.tail map sum )
def fibs2: Stream[Int] = 0 #:: 1 #:: ( fibs2 zip fibs2.tail map sum )
println("========== VAL ============")
println( "----- Take 4:" ); fibs1 take 4 foreach println
println( "----- Take 5:" ); fibs1 take 5 foreach println
println("========== DEF ============")
println( "----- Take 4:" ); fibs2 take 4 foreach println
println( "----- Take 5:" ); fibs2 take 5 foreach println
}
Вот результат:
========== VAL ============
----- Take 4:
0
1
sum (0,1)
1
sum (1,1)
2
----- Take 5:
0
1
1
2
sum (1,2)
3
========== DEF ============
----- Take 4:
0
1
sum (0,1)
1
sum (0,1)
sum (1,1)
2
----- Take 5:
0
1
sum (0,1)
1
sum (0,1)
sum (1,1)
2
sum (0,1)
sum (0,1)
sum (1,1)
sum (1,2)
3
Обратите внимание, что когда мы использовали val:
- "Take 5" не пересчитал значения, вычисленные "take 4".
- Вычисление 4-го значения в "take 4" не вызвало повторного вычисления третьего значения.
Но ни одно из них не является истинным, когда мы используем def. Каждое использование Stream, включая его собственную рекурсию, начинается с нуля новым потоком. Поскольку для получения N-го значения требуется, чтобы мы сначала производили значения для N-1 и N-2, каждый из которых должен создавать свои собственные два предшественника и так далее, количество вызовов sum(), необходимых для создания значения, растет так же, как сама последовательность Фибоначчи: 0, 0, 1, 2, 4, 7, 12, 20, 33,.... И так как все эти потоки находятся в куче одновременно, у нас быстро заканчивается память.
Поэтому, учитывая низкую производительность и проблемы с памятью, вы вообще не хотите использовать def при создании потока.
Но может случиться так, что вы действительно хотите новый Stream каждый раз. Скажем, что вам нужен поток случайных целых чисел, и каждый раз, когда вы обращаетесь к Stream, вам нужны новые целые числа, а не повторение ранее вычисленных целых чисел. И те ранее вычисленные значения, поскольку вы не хотите их повторно использовать, заняли бы место на куче ненужно. В этом случае имеет смысл использовать def, так что вы каждый раз получаете новый поток и не держитесь за него, чтобы его можно было собрать в мусор:
scala> val randInts = Stream.continually( util.Random.nextInt(100) )
randInts: scala.collection.immutable.Stream[Int] = Stream(1, ?)
scala> ( randInts take 1000 ).sum
res92: Int = 51535
scala> ( randInts take 1000 ).sum
res93: Int = 51535 <== same answer as before, from saved values
scala> def randInts = Stream.continually( util.Random.nextInt(100) )
randInts: scala.collection.immutable.Stream[Int]
scala> ( randInts take 1000 ).sum
res94: Int = 49714
scala> ( randInts take 1000 ).sum
res95: Int = 48442 <== different Stream, so new answer
Создание метода randInts заставляет нас получать новый поток каждый раз, поэтому мы получаем новые значения и поток можно собирать.
Обратите внимание, что имеет смысл использовать def здесь, потому что новые значения не зависят от старых значений, поэтому randInts не определяется в терминах самого себя. Stream.continually
- это простой способ создания таких потоков: вы просто рассказываете, как сделать значение, и он создает поток для вас.