【问题标题】:What is the motivation for Scala assignment evaluating to Unit rather than the value assigned?Scala 赋值评估为 Unit 而不是赋值的动机是什么?
【发布时间】:2011-01-01 04:58:20
【问题描述】:

Scala 赋值评估为 Unit 而不是赋值的动机是什么?

I/O 编程中的一个常见模式是这样做:

while ((bytesRead = in.read(buffer)) != -1) { ...

但这在 Scala 中是不可能的,因为...

bytesRead = in.read(buffer)

.. 返回 Unit,而不是 bytesRead 的新值。

离开函数式语言似乎是一件有趣的事情。 我想知道为什么会这样做?

【问题讨论】:

  • 大卫波拉克发布了一些第一手信息,Martin Odersky 本人在他的回答中留下的评论非常赞同。我认为人们可以放心地接受波拉克的回答。

标签: scala functional-programming io assignment-operator


【解决方案1】:

只要你有一个间接引用类型,你就可以有一个解决方法。在幼稚的实现中,您可以将以下内容用于任意类型。

case class Ref[T](var value: T) {
  def := (newval: => T)(pred: T => Boolean): Boolean = {
    this.value = newval
    pred(this.value)
  }
}

然后,在您必须使用ref.value 之后访问引用的约束下,您可以将while 谓词编写为

val bytesRead = Ref(0) // maybe there is a way to get rid of this line

while ((bytesRead := in.read(buffer)) (_ != -1)) { // ...
  println(bytesRead.value)
}

您可以以更隐含的方式对bytesRead 进行检查,而无需键入它。

【讨论】:

    【解决方案2】:

    我不知道具体原因的内幕消息,但我的怀疑很简单。 Scala 使副作用循环难以使用,因此程序员自然会更喜欢 for-comprehensions。

    它以多种方式做到这一点。例如,您没有在其中声明和改变变量的for 循环。在测试条件的同时,您不能(轻松)在 while 循环上改变状态,这意味着您经常必须在它之前和结束时重复突变。在while 块中声明的变量在while 测试条件中不可见,这使得do { ... } while (...) 的用处大大降低。以此类推。

    解决方法:

    while ({bytesRead = in.read(buffer); bytesRead != -1}) { ... 
    

    不惜一切代价。

    作为另一种解释,也许 Martin Odersky 不得不面对一些源自这种用法的非常丑陋的错误,并决定从他的语言中取缔它。

    编辑

    David Pollack 拥有 answered 的一些实际事实,Martin Odersky 本人评论了他的答案这一事实清楚地证明了这一点,这证明了 Pollack 提出的与性能相关的问题论点。

    【讨论】:

    • 所以for 循环版本大概是:for (bytesRead <- in.read(buffer) if (bytesRead) != -1,这很好,只是它不起作用,因为没有foreachwithFilter 可用!
    【解决方案3】:

    顺便说一句:我发现最初的 while-trick 很愚蠢,即使在 Java 中也是如此。为什么不这样呢?

    for(int bytesRead = in.read(buffer); bytesRead != -1; bytesRead = in.read(buffer)) {
       //do something 
    }
    

    当然,赋值出现了两次,但至少 bytesRead 在它所属的范围内,而且我不是在玩有趣的赋值技巧......

    【讨论】:

    • 虽然技巧很常见,但它通常出现在每个通过缓冲区读取的应用程序中。而且它总是看起来像 OP 的版本。
    【解决方案4】:

    也许这是由于command-query separation原则?

    CQS 往往在 OO 和函数式编程风格的交汇处流行,因为它在具有或不具有副作用(即改变对象)的对象方法之间创建了明显的区别。将 CQS 应用于变量赋值比平时更进一步,但同样的想法也适用。

    简要说明 CQS 为何有用:考虑一种假设的混合 F/OO 语言,该语言具有 List 类,该类具有 SortAppendFirstLength 方法。在命令式 OO 风格中,可能想要编写这样的函数:

    func foo(x):
        var list = new List(4, -2, 3, 1)
        list.Append(x)
        list.Sort()
        # list now holds a sorted, five-element list
        var smallest = list.First()
        return smallest + list.Length()
    

    而在更实用的风格中,人们更有可能写这样的东西:

    func bar(x):
        var list = new List(4, -2, 3, 1)
        var smallest = list.Append(x).Sort().First()
        # list still holds an unsorted, four-element list
        return smallest + list.Length()
    

    这些似乎是在试图做同样的事情,但显然两者之一是不正确的,并且在不了解方法的行为的情况下,我们无法判断是哪一个。

    但是,使用 CQS,我们会坚持如果 AppendSort 更改列表,它们必须返回单位类型,从而防止我们在不应该使用第二种形式时创建错误。因此,副作用的存在也隐含在方法签名中。

    【讨论】:

      【解决方案5】:

      我主张让分配返回分配的值而不是单位。 Martin 和我对此反复讨论,但他的论点是,在堆栈上放置一个值只是为了在 95% 的时间内将其弹出是浪费字节码并且对性能产生负面影响。

      【讨论】:

      • Scala编译器无法查看赋值的值是否被实际使用,并据此生成高效的字节码,这是有原因的吗?
      • 在有setter 的情况下并不容易:每个setter 都必须返回一个结果,写起来很痛苦。然后编译器必须对其进行优化,这是跨调用很难做到的。
      • 您的论点确实有道理,但 java 和 C# 反对这一点。我猜你对生成的字节码做了一些奇怪的事情,那么 Scala 中的赋值被编译成类文件并反编译回 Java 会是什么样子?
      • @PhươngNguyễn 区别在于统一访问原则。在 C#/Java 设置器中(通常)返回 void。在 Scala 中,foo_=(v: Foo) 应该返回 Foo 如果赋值。
      • @Martin Odersky: 跟随如何:setter 保持void (Unit),赋值x = value 被翻译成等同于x.set(value);x.get(value);如果值未使用,编译器会在优化阶段消除get-调用。在新的主要版本(由于向后不兼容)Scala 版本中,这可能是一个受欢迎的变化,并且对用户的刺激更少。你怎么看?
      【解决方案6】:

      这发生在 Scala 中,它具有更“形式上正确”的类型系统。正式地说,赋值是一个纯粹的副作用语句,因此应该返回Unit。这确实有一些很好的后果;例如:

      class MyBean {
        private var internalState: String = _
      
        def state = internalState
      
        def state_=(state: String) = internalState = state
      }
      

      state_= 方法返回 Unit(正如 setter 所期望的那样)正是因为赋值返回 Unit

      我同意,对于复制流或类似的 C 样式模式,这个特定的设计决策可能有点麻烦。但是,它实际上总体上相对没有问题,并且确实有助于类型系统的整体一致性。

      【讨论】:

      • 谢谢,丹尼尔。如果一致性是赋值和设置器都返回值,我想我会更喜欢它! (没有理由他们不能。)我怀疑我还没有像“纯粹的副作用陈述”那样去理解概念的细微差别。
      • @Graham:但是,您必须遵循一致性,并确保在所有设置器中,无论它们多么复杂,它们都返回设置的值。我认为这在某些情况下会很复杂,而在其他情况下则是错误的。 (如果出现错误,你会返回什么?null?——而不是。None?——那么你的类型将是 Option[T]。)我认为很难保持一致。
      【解决方案7】:

      将赋值用作布尔表达式并不是最好的风格。您同时执行两件事,这通常会导致错误。 Scalas 限制避免了意外使用“=”而不是“==”。

      【讨论】:

      • 我认为这是一个垃圾理由!正如 OP 发布的那样,代码仍然可以编译和运行:它只是没有做你可能合理期望的事情。多一个陷阱,而不是少一个!
      • 如果你写类似 if(a = b) 的东西,它不会编译。所以至少可以避免这个错误。
      • OP 没有使用 '=' 而不是 '==',他同时使用了两者。他希望赋值返回一个值,然后可以使用该值,例如,与另一个值(示例中的 -1)进行比较
      • @deamon:如果 a 和 b 是布尔值,它将编译(至少在 Java 中)。我已经看到新手使用 if (a = true) 掉进了这个陷阱。还有一个理由更喜欢更简单的 if (a)(如果使用更重要的名称,则更清晰!)。
      【解决方案8】:

      我猜这是为了保持程序/语言没有副作用。

      您所描述的是故意使用在一般情况下被认为是坏事的副作用。

      【讨论】:

      • 嘿。 Scala 没有副作用? :) 另外,想象一个像val a = b = 1 (想象valb 前面的“神奇”val)与val a = 1; val b = 1; 的情况。
      • 这与副作用无关,至少在此处描述的意义上不是:Side effect (computer science)
      猜你喜欢
      • 2016-03-14
      • 2017-09-23
      • 1970-01-01
      • 2011-08-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多