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

8

85

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

A common pattern in I/O programming is to do things like this:

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

But this is not possible in Scala because...

bytesRead = in.read(buffer)

.. returns Unit, not the new value of bytesRead.

Seems like an interesting thing to leave out of a functional language. I am wondering why it was done so?

Shed answered 4/1, 2010 at 10:37 Comment(1)
David Pollack has posted some first-hand information, pretty much endorsed by the comment Martin Odersky himself left on his answer. I think one can safely acceppt Pollack's answer.Calends
C
92

I advocated for having assignments return the value assigned rather than unit. Martin and I went back and forth on it, but his argument was that putting a value on the stack just to pop it off 95% of the time was a waste of byte-codes and have a negative impact on performance.

Cuvette answered 4/1, 2010 at 16:18 Comment(7)
Is there a reason why the Scala compiler could not look at whether the value of the assignment is actually used, and generate efficient bytecode accordingly?Morita
It's not so easy in the presence of setters: Every setter has to return a result, which is a pain to write. Then the compiler has to optimize it away, which is hard to do across calls.Cariecaries
Your argument does make sense, yet java & C# are against that. I guess you are doing something weird with the generated byte code, then how would an assignment in Scala being compiled into class file and the decompiled back to Java look like?Triazine
@PhươngNguyễn The difference is Uniform Access Principle. In C#/Java setters (usually) return void. In Scala foo_=(v: Foo) should return Foo if assignment does.Mantellone
@Martin Odersky: how about following: setters remain void (Unit), assignments x = value get translated into equivalent of x.set(value);x.get(value); the compiler eliminates in optimizing phases the get-calls if the value was unused. It could be a welcome change in a new major (because of backward incompatibility) Scala release and fewer irritations for users. What do you think?Enow
Should it be possible for the languages optimizer to eliminate the 95% of unused cases?Urata
All the ugliness of the first programming languages was because of problems with the generated code. FORTRAN used to prohibit recursion. Pascal required you to specify the maximal string length in advance (and GC was considered too complex). Semi-colons were necessary because they facilitated parsing. The NULL pointer was introduced just because it was easy to implement. Classification Sort was not taught in CS courses because it required too much memory for indexes, about 300K. OMG, elimination of push/pop-s was called pinhole optimization in 1990's.Levey
C
21

I'm not privy to inside information on the actual reasons, but my suspicion is very simple. Scala makes side-effectful loops awkward to use so that programmers will naturally prefer for-comprehensions.

It does this in many ways. For instance, you don't have a for loop where you declare and mutate a variable. You can't (easily) mutate state on a while loop at the same time you test the condition, which means you often have to repeat the mutation just before it, and at the end of it. Variables declared inside a while block are not visible from the while test condition, which makes do { ... } while (...) much less useful. And so on.

Workaround:

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

For whatever it is worth.

As an alternate explanation, perhaps Martin Odersky had to face a few very ugly bugs deriving from such usage, and decided to outlaw it from his language.

EDIT

David Pollack has answered with some actual facts, which are clearly endorsed by the fact that Martin Odersky himself commented his answer, giving credence to the performance-related issues argument put forth by Pollack.

Calends answered 4/1, 2010 at 11:57 Comment(1)
So presumably the for loop version would be: for (bytesRead <- in.read(buffer) if (bytesRead) != -1 which is great except that it won't work because there's no foreach and withFilter available!Margy
L
12

This happened as part of Scala having a more "formally correct" type system. Formally-speaking, assignment is a purely side-effecting statement and therefore should return Unit. This does have some nice consequences; for example:

class MyBean {
  private var internalState: String = _

  def state = internalState

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

The state_= method returns Unit (as would be expected for a setter) precisely because assignment returns Unit.

I agree that for C-style patterns like copying a stream or similar, this particular design decision can be a bit troublesome. However, it's actually relatively unproblematic in general and really contributes to the overall consistency of the type system.

Lucan answered 4/1, 2010 at 15:14 Comment(2)
Thanks, Daniel. I think I would prefer it if the consistency were that both assignments AND setters returned the value! (There's no reason they can't.) I suspect I'm not grokking the nuances of concepts like a "purely side-effecting statement" just yet.Shed
@Graham: But then, you’d have to follow the consistency and ensure in all your setters however complex they might be, that they return the value they set. This would be complicated in some cases and in other cases just wrong, I think. (What would you return in case of error? null? – rather not. None? – then your type will be Option[T].) I think it’s hard being consistent with that.Donaugh
G
7

Perhaps this is due to the command-query separation principle?

CQS tends to be popular at the intersection of OO and functional programming styles, as it creates an obvious distinction between object methods that do or do not have side-effects (i.e., that alter the object). Applying CQS to variable assignments is taking it further than usual, but the same idea applies.

A short illustration of why CQS is useful: Consider a hypothetical hybrid F/OO language with a List class that has methods Sort, Append, First, and Length. In imperative OO style, one might want to write a function like this:

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()

Whereas in more functional style, one would more likely write something like this:

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()

These seem to be trying to do the same thing, but obviously one of the two is incorrect, and without knowing more about the behavior of the methods, we can't tell which one.

Using CQS, however, we would insist that if Append and Sort alter the list, they must return the unit type, thus preventing us from creating bugs by using the second form when we shouldn't. The presence of side effects therefore also becomes implicit in the method signature.

Generative answered 4/1, 2010 at 16:38 Comment(0)
T
4

I'd guess this is in order to keep the program / the language free of side effects.

What you describe is the intentional use of a side effect which in the general case is considered a bad thing.

Tempe answered 4/1, 2010 at 10:42 Comment(2)
Heh. Scala free of side-effects? :) Also, imagine a case like val a = b = 1 (imagine "magical" val in front of b) vs. val a = 1; val b = 1;.Bracken
This has nothing to do with side effects, at least not in the sense described here: Side effect (computer science)Electrometallurgy
T
4

It is not the best style to use an assignment as a boolean expression. You perform two things at the same time which leads often to errors. And the accidential use of "=" instead of "==" is avoided with Scalas restriction.

Trounce answered 4/1, 2010 at 11:8 Comment(4)
I think this is a rubbish reason! As the OP posted, the code still compiles and runs: it just doesn't do what you might reasonably expect it to. It's one more gotcha, not one less!Margy
If you write something like if(a = b) it will not compile. So at least this error can be avoided.Trounce
The OP did not use '=' instead of '==', he used both. He expects the assignment to return a value which can then be used, e.g., to compare to another value (-1 in the example)Peep
@deamon: it will compile (in Java at least) if a and b are boolean. I have seen newbies falling on this trap by using if (a = true). One more reason to prefer the simpler if (a) (and clearer if using a more significant name!).Rodrigues
D
2

By the way: I find the initial while-trick stupid, even in Java. Why not somethign like this?

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

Granted, the assignment appears twice, but at least bytesRead is in the scope it belongs to, and I'm not playing with funny assignment tricks...

Disenfranchise answered 4/1, 2010 at 21:55 Comment(1)
That while trick is a pretty common one, it usually appears in every app which reads through a buffer. And it always looks like OP's version.Lema
D
0

You can have a workaround for this as long as you have a reference type for indirection. In a naïve implementation, you can use the following for arbitrary types.

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

Then, under the constraint that you’ll have to use ref.value to access the reference afterwards, you can write your while predicate as

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

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

and you can do the checking against bytesRead in a more implicit manner without having to type it.

Donaugh answered 18/6, 2010 at 23:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.