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

What is the motivation for Scala assignment evaluating to Unit rather than the value assigned?

提问人:Graham Lea 提问时间:1/4/2010 更新时间:6/19/2010 访问量:8532

问:

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

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

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

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

bytesRead = in.read(buffer)

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

从函数式语言中省略似乎是一件有趣的事情。 我想知道为什么要这样做?

Scala 函数编程 IO 赋值运算符

评论

0赞 Daniel C. Sobral 1/5/2010
大卫·波拉克(David Pollack)发布了一些第一手信息,马丁·奥德斯基(Martin Odersky)本人在他的回答中留下的评论几乎得到了认可。我认为人们可以放心地接受波拉克的答案。

答:

4赞 Jens Schauder 1/4/2010 #1

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

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

评论

0赞 12/11/2011
嘿。Scala 没有副作用?:)另外,想象一个案例,例如(想象一下前面的“神奇”)与.val a = b = 1valbval a = 1; val b = 1;
0赞 Feuermurmel 8/3/2018
这与副作用无关,至少与此处描述的意义上无关:副作用(计算机科学)
4赞 deamon 1/4/2010 #2

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

评论

2赞 oxbow_lakes 1/4/2010
我认为这是一个垃圾理由!正如 OP 所发布的那样,代码仍在编译和运行:它只是没有做你可能合理期望的事情。这又是一个陷阱,而不是一个!
1赞 deamon 1/4/2010
如果你写类似 if(a = b) 的东西,它将无法编译。所以至少可以避免这个错误。
1赞 IttayD 1/4/2010
OP 没有使用“=”而不是“==”,他同时使用了两者。他希望赋值返回一个值,然后可以使用该值,例如,与另一个值进行比较(在示例中为 -1)
0赞 PhiLho 10/21/2011
@deamon:如果 a 和 b 是布尔值,它将编译(至少在 Java 中)。我见过新手通过使用 if (a = true) 落入这个陷阱。如果 (a) 更喜欢更简单的另一个原因(如果使用更重要的名称,则更清晰!
21赞 Daniel C. Sobral 1/4/2010 #3

我不了解实际原因的内幕消息,但我的怀疑很简单。Scala 使副作用循环使用起来很笨拙,因此程序员自然会更喜欢理解。

它以多种方式做到这一点。例如,你没有一个循环来声明和改变一个变量。你不能(轻易地)在测试条件的同时改变循环上的状态,这意味着你经常必须在它之前和结束时重复突变。在块内声明的变量在测试条件中不可见,这使得它的用处要小得多。等等。forwhilewhilewhiledo { ... } while (...)

解决方法:

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

无论它值多少钱。

作为另一种解释,也许马丁·奥德斯基(Martin Odersky)不得不面对这种用法产生的一些非常丑陋的错误,并决定从他的语言中取缔它。

编辑

大卫·波拉克(David Pollack)用一些实际事实来回答马丁·奥德斯基(Martin Odersky)本人评论了他的答案,这一事实清楚地证实了波拉克提出的与绩效有关的问题论点。

评论

3赞 oxbow_lakes 1/4/2010
所以大概循环版本是:这很棒,只是它不起作用,因为没有和可用!forfor (bytesRead <- in.read(buffer) if (bytesRead) != -1foreachwithFilter
12赞 Daniel Spiewak 1/4/2010 #4

这是 Scala 拥有更“形式正确”的类型系统的一部分。从形式上讲,赋值是一个纯粹的副作用语句,因此应该返回 。这确实有一些很好的后果;例如:Unit

class MyBean {
  private var internalState: String = _

  def state = internalState

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

该方法返回(正如 setter 所期望的那样),正是因为 assignment 返回 .state_=UnitUnit

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

评论

0赞 Graham Lea 1/5/2010
谢谢,丹尼尔。我想我更喜欢一致性是赋值和二传手都返回了值!(他们没有理由不能。我怀疑我还没有像“纯粹的副作用陈述”那样摸索概念的细微差别。
2赞 Debilski 6/19/2010
@Graham:但是,你必须遵循一致性,并确保在你所有的二传手中,无论它们多么复杂,它们都返回他们设置的值。我认为,在某些情况下,这会很复杂,而在其他情况下,这是错误的。(如果出现错误,你会返回什么?null?——而不是。 None?——那么你的类型将是 Option[T]。我认为很难与此保持一致。
92赞 David Pollak 1/5/2010 #5

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

评论

7赞 Matt R 1/5/2010
Scala 编译器无法查看赋值的值是否实际使用,并相应地生成高效的字节码,这有什么原因吗?
43赞 Martin Odersky 1/5/2010
在二传手面前,这并不容易:每个二传手都必须返回一个结果,这写起来很痛苦。然后编译器必须对其进行优化,这在跨调用中很难做到。
1赞 Phương Nguyễn 7/26/2010
你的论点确实有道理,但 java 和 C# 反对这一点。我猜你正在用生成的字节码做一些奇怪的事情,那么 Scala 中的赋值被编译成类文件并反编译回 Java 会是什么样子?
3赞 Alexey Romanov 12/11/2011
@PhươngNguyễn 区别在于统一访问原则。在 C#/Java 中,setters(通常)返回 .在 Scala 中,如果赋值确实如此,则应返回。voidfoo_=(v: Foo)Foo
5赞 Eugen Labun 2/13/2012
@Martin Odersky:如何遵循:二传手保持(),赋值被翻译成等价的;如果值未使用,编译器会在优化阶段消除 -调用。这可能是一个新的主要(因为向后不兼容)Scala 版本的一个可喜变化,并且对用户的烦恼更少。你觉得怎么样?voidUnitx = valuex.set(value);x.get(value)get
7赞 C. A. McCann 1/5/2010 #6

也许这是由于命令-查询分离原则?

CQS 往往在面向对象和函数式编程风格的交集上很受欢迎,因为它在有或没有副作用(即改变对象)的对象方法之间创建了明显的区别。将 CQS 应用于变量赋值比平时更进一步,但同样的想法也适用。

CQS 为何有用的简短说明:考虑一个假设的混合 F/OO 语言,其类具有方法 、 、 和 。在命令式 OO 风格中,人们可能想要编写一个这样的函数:ListSortAppendFirstLength

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

2赞 Landei 1/5/2010 #7

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

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

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

评论

1赞 TWiStErRob 11/15/2015
虽然技巧是一种非常常见的技巧,但它通常出现在每个通过缓冲区读取的应用程序中。而且它总是看起来像 OP 的版本。
0赞 Debilski 6/19/2010 #8

只要您有间接引用类型,就可以对此进行解决。在朴素实现中,可以对任意类型使用以下内容。

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

然后,在之后必须用于访问引用的约束下,可以将谓词写为ref.valuewhile

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