将Scala分配评估为单位而不是赋值的动机是什么?


84

将Scala分配评估为单位而不是赋值的动机是什么?

I / O编程中的常见模式是执行以下操作:

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

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

bytesRead = in.read(buffer)

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

忽略功能性语言似乎是一件有趣的事情。我想知道为什么要这么做吗?


大卫·波拉克(David Pollack)发布了一些第一手资料,马丁·奥德斯基(Martin Odersky)本人在回答中留下的评论对此表示赞同。我认为可以安全地接受Pollack的回答。
Daniel C. Sobral

Answers:


88

我主张让赋值返回赋值而不是单位。Martin和我来回地讨论,但是他的论点是在95%的时间中将值放到堆栈上只是为了弹出它是浪费字节码,并且对性能产生负面影响。


7
Scala编译器无法查看分配值是否实际使用,并据此生成有效的字节码,这是有原因的吗?
Matt R

43
在有setter的情况下并不是那么容易:每个setter必须返回一个结果,这是很痛苦的。然后,编译器不得不对其进行优化,这在调用之间很难做到。
马丁·奥德斯基

1
您的论点确实有道理,但是Java和C#反对。我猜您正在用生成的字节码做一些奇怪的事情,那么Scala中的赋值被编译成类文件并反编译回Java会是什么样子?
PHUONG阮

3
@PhươngNguyễn区别在于统一访问原则。在C#/ Java setter中(通常)return void。如果分配确实在Scala中,foo_=(v: Foo)则应返回Foo
阿列克谢·罗曼诺夫

5
@Martin Odersky:以下内容如何:设置器为voidUnit),作业x = value被翻译为x.set(value);x.get(value)get如果未使用该值,编译器将在优化阶段中消除-calls。在新的主要版本(由于向后不兼容)的Scala版本中,这可能是一个可喜的变化,并且对用户的烦恼也减少了。你怎么看?
Eugen Labun '02

20

我不了解内幕消息的真实原因,但我的怀疑很简单。Scala难以使用副作用循环,因此程序员自然会偏向于理解。

它以多种方式做到这一点。例如,您没有for声明和突变变量的循环。while在测试条件的同时,您不能(轻松)在循环上对状态进行突变,这意味着您经常必须在突变之前和之后重复突变。whilewhile测试条件do { ... } while (...)中看不到在块内声明的变量,这使它的用处大大减少。等等。

解决方法:

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

不管它是值得的。

作为另一种解释,也许马丁·奥德斯基不得不面对源自这种用法的一些非常丑陋的错误,并决定将其从他的语言中取缔。

编辑

大卫·波拉克(David Pollack回答了一些实际事实,马丁·奥德斯基(Martin Odersky)亲自评论了他的答案,这一事实得到了明显的支持,这使波拉克(Pollack)提出了与绩效相关的问题。


3
所以想必for环路版本将是:for (bytesRead <- in.read(buffer) if (bytesRead) != -1这是不同的,它不会工作的伟大,因为没有foreachwithFilter可用!
oxbow_lakes

12

这是Scala具有更“形式正确”的类型系统的一部分。从形式上讲,赋值是一个纯粹的副作用声明,因此应该返回Unit。这确实会带来一些不错的后果。例如:

class MyBean {
  private var internalState: String = _

  def state = internalState

  def state_=(state: String) = internalState = state
}

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

我同意对于复制流或类似内容的C样式模式,此特定的设计决策可能会有些麻烦。但是,它实际上一般来说没有问题,并且确实有助于类型系统的整体一致性。


谢谢,丹尼尔。我想如果一致是赋值和设置器都返回值,我会更喜欢它!(没有理由他们不能这样做。)我怀疑我现在还没有在理解诸如“纯粹有副作用的声明”之类的概念的细微差别。
Graham Lea 2010年

2
@Graham:但是,接下来,您必须遵循一致性,并确保所有设置器中无论它们多么复杂,都必须返回设置的值。我认为这在某些情况下会很复杂,而在另一些情况下只是错了。(如果发生错误,您将返回什么?null?–而不是。None?–那么您的类型将是Option [T]。)我认为很难与之保持一致。
Debilski 2010年

7

也许这是由于命令查询分离原理造成的?

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

为什么CQS是有用的简短说明:考虑用一个假设的混合动力F / OO语言List有方法的类SortAppendFirst,和Length。在命令式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改变列表,他们必须返回单位类型,从而通过时,我们不应该用第二种形式阻止我们创造的错误。因此,副作用的存在也隐含在方法签名中。


4

我想这是为了保持程序/语言不受副作用。

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


嘿。Scala没有副作用吗?:)另外,请设想一下类似的情况val a = b = 1val在前面想象“魔术” bval a = 1; val b = 1;

这与副作用无关,至少与此处描述的意义无关:副作用(计算机科学)
Feuermurmel

4

将赋值用作布尔表达式不是最好的样式。您同时执行两项操作,这通常会导致错误。Scalas限制可以避免意外使用“ =”而不是“ ==”。


2
我认为这是垃圾原因!正如OP所张贴的那样,代码仍然可以编译和运行:它没有执行您可能合理期望的操作。这是更多的陷阱,而不是更少!
oxbow_lakes

1
如果您编写类似if(a = b)的内容,它将无法编译。因此,至少可以避免该错误。
迪蒙

1
OP没有使用'='而不是'==',他都使用了。他希望分配返回一个值,然后可以使用该值,例如,与另一个值进行比较(在示例中为-1)
IttayD 2010年

@deamon:如果a和b为布尔值,它将编译(至少在Java中)。我已经看到新手通过使用if(a = true)落入这个陷阱。如果(a)更喜欢更简单的一个原因(如果使用更重要的名称则更清楚!)。
PhiLho 2011年

2

顺便说一句:即使在Java中,我也会发现最初的“同时欺骗”的愚蠢行为。为什么不这样呢?

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

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


1
尽管技巧很普遍,但它通常会出现在每个读取缓冲区的应用程序中。它总是看起来像OP的版本。
TWiStErRob

0

只要您有间接引用类型,就可以解决此问题。在简单的实现中,您可以将以下内容用于任意类型。

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以更隐式的方式进行检查,而不必键入它。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.