【问题标题】:Should I use val or def when defining a Stream?定义 Stream 时应该使用 val 还是 def?
【发布时间】:2012-10-24 09:50:40
【问题描述】:

在回答 StackOverflow 问题时,我创建了一个 Stream 作为 val,如下所示:

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

有人告诉我应该使用 def 而不是 val 因为 Scala Kata 抱怨(就像 Eclipse 中的 Scala Worksheet 一样)“前向引用扩展了定义值 s。”

但 Stream 文档中的示例使用 val。哪个是对的?

【问题讨论】:

    标签: scala stream


    【解决方案1】:

    只要变量是类的字段而不是局部变量,Scalac 和 REPL 就可以使用该代码(使用 val)。您可以使变量变得惰性以满足 Scala Kata 的要求,但您通常不希望在实际程序中以这种方式使用 def(即,就其本身而言 def 是一个 Stream)。如果这样做,则每次调用该方法时都会创建一个新的 Stream,因此以前的计算结果(保存在 Stream 中)永远不会被重用。如果你从这样一个 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”计算的值。
    • 计算“take 4”中的第 4 个值不会导致重新计算第 3 个值。

    但是当我们使用 def 时,这些都不正确。 Stream 的每次使用,包括它自己的递归,都从一个新的 Stream 开始。由于产生第 N 个值需要我们首先产生 N-1 和 N-2 的值,每个值都必须产生自己的两个前任,依此类推,产生一个值所需的 sum() 调用次数的增长很像斐波那契数列本身:0, 0, 1, 2, 4, 7, 12, 20, 33, .... 由于所有这些 Streams 同时在堆上,我们很快就会耗尽内存。

    因此,鉴于性能不佳和内存问题,您通常不希望在创建 Stream 时使用 def。

    但您实际上可能确实每次都想要一个新的 Stream。假设您需要一个随机整数流,并且每次访问该流时都需要新的整数,而不是以前计算的整数的重放。而那些先前计算的值,因为您不想重用它们,会不必要地占用堆上的空间。在这种情况下,使用 def 是有意义的,这样你每次都会获得一个新的 Stream 而不要一直持有它,这样它就可以被垃圾收集:

    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做成一个方法会导致我们每次都得到一个新的Stream,所以我们得到了新的值,这个Stream就可以被收集了。

    请注意,这里只使用 def 才有意义,因为新值不依赖于旧值,因此 randInts 不是根据自身定义的。 Stream.continually 是生成此类 Streams 的一种简单方法:您只需告诉它如何生成值,它就会为您生成 Stream。

    【讨论】:

    • 您确定这是演示编译器的限制,而不仅仅是字段与局部变量的关系吗?
    • 好点,路易吉。阅读您的评论后,我做了更多的实验,现在我认为我不完全理解问题所在,但我认为这与这些工具包装代码的方式有关。我在 Scala 工作表中的对象中得到错误,但在类中没有,并且两者都使用 scalac。我会修改答案不怪PC。
    • 如果你用一个对象包裹它就可以了:scalakata.com/50975187e4b093f3524f3685
    猜你喜欢
    • 1970-01-01
    • 2017-01-04
    • 2015-05-13
    • 1970-01-01
    • 2013-11-05
    • 2020-03-15
    • 1970-01-01
    • 2017-06-21
    • 1970-01-01
    相关资源
    最近更新 更多