【问题标题】:Why is "lazy" a keyword rather than a standard-library type?为什么“懒惰”是关键字而不是标准库类型?
【发布时间】:2014-01-13 14:44:27
【问题描述】:

Scala 在其标准库中保留了许多非常有用的结构,例如 Option 和 Try。

为什么在缺少上述类型的 C# 等语言选择将其作为库功能实现时,通过拥有自己的关键字来给予 lazy 特殊处理?

【问题讨论】:

    标签: scala keyword lazy-evaluation standard-library


    【解决方案1】:

    确实可以像这样定义一个惰性值:

    object Lazy {  
      def apply[A](init: => A): Lazy[A] = new Lazy[A] {
        private var value = null.asInstanceOf[A]
        @volatile private var initialized = false
    
        override def toString = 
          if (initialized) value.toString else "<lazy>@" + hashCode.toHexString
    
        def apply(): A = {
          if (!initialized) this.synchronized {
            if (!initialized) {
              value = init
              initialized = true
            }
          }
          value
        }
      }
    
      implicit def unwrap[A](l: Lazy[A]): A = l()
    }     
    
    trait Lazy[+A] { def apply(): A }
    

    用法:

    val x = Lazy {
      println("aqui")
      42
    }
    
    def test(i: Int) = i * i
    
    test(x)
    

    另一方面,将lazy 作为语言提供的修饰符具有允许它参与统一访问原则的优势。我试图为它查找博客条目,但没有任何超越 getter 和 setter 的内容。这个原则其实更为根本。对于值,统一如下:vallazy valdefvarobject

    trait Foo[A] {
      def bar: A
    }
    
    class FooVal[A](val bar: A) extends Foo[A]
    
    class FooLazyVal[A](init: => A) extends Foo[A] {
      lazy val bar: A = init
    }
    
    class FooVar[A](var bar: A) extends Foo[A]
    
    class FooProxy[A](peer: Foo[A]) extends Foo[A] {
      def bar: A = peer.bar
    }
    
    trait Bar {
      def baz: Int
    }
    
    class FooObject extends Foo[Bar] {
      object bar extends Bar {
        val baz = 42
      }
    }
    

    Scala 2.6 中引入了惰性值。有一个Lambda the Ultimate comment 表明推理可能与形式化具有循环引用的可能性有关:

    循环依赖需要绑定惰性值。惰性值也可用于强制组件初始化按依赖顺序进行。遗憾的是,组件关闭命令必须手动编码

    我不知道为什么编译器不能自动处理循环引用;也许有复杂性或性能惩罚的原因。 blog post by Iulian Dragos 证实了其中一些假设。

    【讨论】:

      【解决方案2】:

      当前的惰性实现使用 int 位掩码来跟踪字段是否已初始化,并且没有其他内存开销。该字段在多个惰性值之间共享(每个字段最多 32 个惰性值)。以与库功能相似的内存效率来实现该功能是不可能的。

      Lazy 作为一个库大概看起来像这样:

      class LazyVal[T](f: =>T) {
        @volatile private var initialized = false
      
        /*
         this does not need to be volatile since there will always be an access to the
         volatile field initialized before this is read.
        */
        private var value:T = _ 
        def apply() = {
          if(!initialized) {
            synchronized {
              if(!initialized) {
                value = f
                initialized = true
              }
            }
          }
          value
        }
      }
      

      这样做的开销将是一个用于生成值的闭包 f 的对象,以及另一个用于 LazyVal 本身的对象。因此,对于像这样经常使用的功能来说,这将是非常重要的。

      在 CLR 上,您有值类型,因此如果您将 LazyVal 实现为 C# 中的结构,则开销不会那么糟糕

      但是,既然宏可用,将惰性转换为库功能或至少允许自定义惰性初始化可能是个好主意。 lazy val 的很多用例不需要线程同步,所以每次使用lazy val 时都有@volatile/synchronized 开销是很浪费的。

      【讨论】:

      • 我想知道是否可以使用宏注释来实现@lazy。
      • 我不知道这是否可能,但它肯定会有用。惰性值有许多不同的用例。例如,如果您出于性能原因仅使用惰性 val 缓存纯函数,则您不关心该值是否可能在竞争场景中计算两次。所以你可以放弃同步。但是如果你使用lazy val 来管理一些外部资源,你确实很在意。显然默认行为应该仍然尽可能安全,但是一些自定义会非常好......
      • 使用关键字将变量标记为惰性具有额外的好处,即能够在不真正触及正文的情况下更改现有语句的行为。通过仅更改关键字,其他实体将被忽略并且主体保持不变。在其他实现中,您总是需要在正确的范围内添加一些惰性“块”或隐式定义。 Macros 这句话听起来很有趣,提供不同口味的懒惰可能是一件值得考虑的事情。 @Eugene 在这里的好事 :)
      • @EugeneBurmako - 想想这可能很有趣——但我认为这不是一个好主意。编译器应该可以忽略注释,但在很多使用lazy 的情况下,忽略它并使其变得急切会产生完全不同的结果。
      • 为什么注解应该被编译器忽略?
      猜你喜欢
      • 2013-03-15
      • 1970-01-01
      • 1970-01-01
      • 2015-09-06
      • 2019-10-15
      • 1970-01-01
      • 2021-08-12
      • 1970-01-01
      • 2011-08-01
      相关资源
      最近更新 更多