Должен ли я использовать val или def при определении потока?

В ответе на вопрос StackOverflow я создал Stream как val, например:

val s:Stream[Int] = 1 #:: s.map(_*2)

и кто-то сказал мне, что вместо val следует использовать def, потому что Scala Kata жалуется (как и на Scala Worksheet in Eclipse), что "вперед ссылка распространяется на определение значения s."

Но в примерах в потоке docs используется val. Какой из них прав?

Ответ 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 - это простой способ создания таких потоков: вы просто рассказываете, как сделать значение, и он создает поток для вас.