This is exactly what I’m afraid of. People often opt for the shortest way to write a function and def foo() = { ... }
is shorter than def foo(): Unit = { ... }
, but not specifying a return type on a public method and relying on type inference is an anti-pattern. def foo() { ... }
is both concise and safe.
A small step in actual syntax change, a big step towards more consistency.
Exactly the same feedback on my part.
Because if most of the code base uses the standard notation and someone not familiar with procedure syntaxe try to change à method which use it, he may miss it and loose quite some time with strange errors.
I eveb get caught regularly on other code base which use it even if I know it exists.
No, the problem is not that the linter rise an error, but that the linter goal is lost: you don’t have the return type for the method apparent.
And it’s especially inconsistent (and hard to get for new user) when you have a virtual method.
As Jorge says, if conciseness is your priority, it’s one added character. (I personally think that side-effecting functions should have an explicit type ascription, but AFAIK nobody’s saying that would be required.) I have a lot of trouble understanding this level of fury about that.
As for migration: you’re clear that we’re talking about Scala 3, right? You will have to migrate your codebase. The migration shouldn’t be terrible (fingers crossed), but it’s gonna be necessary. So getting this worked up about one of the smallest of the changes feels disproportionate to me.
Oh, that one’s also a huge gotcha. But I haven’t seen anyone proposing the elimination of foreach
, so I’m not going to worry about it.
Fair enough, but I’m precisely the opposite – this one’s driven me pretty crazy a few times in the past, because I don’t find the missing =
at all glanceable.
I’m not taking sides but I want to just point out that an individual “special case” on its own may not be that much but in the aggregate they really add up. One of the promises of Dotty is to make scala much more regular. Let’s keep that in mind.
Sorry I just skimmed the long thread. I have followed @Ichoran 's view on this.
How about deprecate result type Unit
and encourage procedure syntax for that case?
It’s a total flip-flop from the current change.
That is, it’s not just syntax. Unit-valued methods are somehow distinct, so require the syntax to be distinct also. def f(): Unit
was hip for no reason, it’s a different animal.
This would also contribute to the debate about Unit-valued method having an empty parameter list.
The OED will credit @jducoeur with glanceable
.
I deal with teaching new people Scala and this is something that always comes up as a bit confusing and inconsistent. Beginners make mistakes trying to return non-unit values from procedure syntax definitions (especially former Java programmers) and the compiler error confuses them. Or they don’t realize that ) {
and ) = {
are quite different. Beginners forget to ascribe return values and get confusing compiler messages when terminal branches of pattern maches or if/else have incompatible or ambiguous types. The answer I give beginners is to ALWAYS ascribe types on your defs until you’re very comfortable with the language and how its type inference works. Dropping return types from non-trivial definitions (perhaps 4 lines or more) is for advanced users. Even for advanced users, it can be very tricky to ‘see’ the types that result from a big pattern match or long chained flow with complicated types. For consistency sake, do the same for Unit as other types.
Making it harder to refactor something to/from Unit and another type is also unfortunate.
As for the argument that this makes side effects easier to see, I disagree strongly. Lots of side-effecting actions return values. Its of little help to pay attention to whether it returns a value or not. Just look at mutable.Map or mutable.Set ! Its best practice even in mutable / effectful contexts to return values rather than nothing at all when possible.
I guess I wouldn’t be opposed to allowing procedure syntax for private methods on these grounds, but then the inconsistency is even more niche.
Its not the same, but I recall a blog post about how awful scala was because it put the name before the type and how the type being first was more ‘natural’. Sure, if you’ve done something one way for a decade, then shifting is a little bit jarring. This is going to get some people out of their comfort zone and of course the transition is going to affect how easy things are to read. But that is transient. At least it was for me. The change to remove procedure syntax from my code looked strange at first, but after a couple months its code with procedure syntax that looks strange.
Now, i really like the consistency that ALL definitions have a =
separating the declaration and implementation, and without =
it is undefined. Its consistent from val x = 3
to def x = 3
to val x = println("Hello")
to def x = println("hello")
. My eye is now trained to look for the =
. Anything without it looks wrong. Maybe I can repurpose the neurons that parse procedure syntax to do something else.
That seems unambiguously side-effecting, because of the empty argument list, whether it returns Unit
or not (I’m not sure if Unit
-returning is as important as side-effecting).
But if foo
takes at least one non-implicit argument, it’s actually a somewhat valid example for your point. And in that case, the change adds indeed 8 characters.
I must say, tho, that IntelliJ inserts (as annotations) both inferred return types and keyword-argument names, which is extremely satisfying. Of course, it’s debatable whether we should assume a sufficiently smart IDE till one is there (like a sufficiently smart compiler), tho VSCode/LSP/DottyIDE is promising.
Blush, but IIRC it’s fairly common UX jargon. (At least, I’m pretty sure that’s where I got it from.)
Thanks, now I googled it. I guess UX means UI but after Brexit?
I have gone back and forth on Unit methods, especially in the context of unit tests that throw (and the coincidence of Unit and unit here has to be remarked).
I hope the community recognizes ichoran’s take and considers restoring procedure syntax and outlawing Unit-valued methods. Ha! Then the tables have turned.
I just thought that removing procedure syntax also means prohibiting the following syntax for abstract methods:
def foo(x: Int)
def bar()
def foobar
all of which are currently accepted. They would need to be replaced by
def foo(x: Int): Unit
def bar(): Unit
def foobar: Unit
Not saying it’s bad. I’m just pointing out a detail that was never mentioned, AFAICT.
How much do you use Unit-returning functions? My hypothesis was that the people who don’t often use no-return side-effecting methods are the ones who get tripped up.
I agree that’s a valuable goal, and is worth sacrificing some convenience for.
Do you clearly teach them the difference between these things, or ignore it until they hit it and are confused? I agree that this is one of a number of things that requires training to understand. Having fewer things is certainly a win for training. The question is whether there’s a downside, and if so, how big of one.
Yes, they do, and those methods are harder to figure out.
Why? This makes everything harder to reason about. Of course you have to if there’s a common failure mode or you’re doing a combined mutation/creation operation. But unless really required, I find return values (other than this.type
) an antipattern. (I’m not sure the lore is that it’s an antipattern, but I write a lot of mutable code and in practice I have the easiest time understanding it when mutations are kept as cleanly separate from return values as possible.) Silently discarding return values is common when you don’t really care, but ignoring them when you should is dreadful source of error. If there isn’t something important to return, I’d rather have it not be there, and keep the interface clean. And I’d prefer to warn if the return value is ignored.
So it really jumps out at you when it’s missing. Which is…something like what I’ve been saying all along?
Not for me. I can handle it both ways, but the : Unit =
thing is unambiguously worse. It’s not very much worse, but I will always notice. I also notice that void thingy()
in Java or C/C++ doesn’t jump out the way it should. I also find fn foo() { .. }
in Rust much more obvious than fn foo() -> () { ... }
, and appreciate that the former is available.
And some of the code I have does this a lot. I wonder whether anyone who uses this pattern a lot prefers : Unit =
?
If we’re going to make changes like this, I think it’s important to clearly understand the tradeoffs rather than pretending that some sacrifices aren’t sacrifices. Among other things, if one doesn’t understand the sacrifices, it’s less likely that they’ll be mitigated.
The advantages seem to be
- Having only one syntax is easier to learn.
- The
): Unit = {
syntax is more visually obvious to some people. - Having fewer special cases simplifies the language overall.
The disadvantages seem to be
- Having to relearn a syntax is harder than not having to relearn it.
- The brevity of
) {
syntax is appreciated by some people. - Some people fear an increase in
) = {
which actually just returnsUnit
.
The respective points 1 are simply a head-to-head conflict. Relearning is harder than learning, but you can argue that it’s more important to be accessible initially.
The respective points 2 are also in head-to-head conflict, but it is not really determined yet how big of an advantage/burden it is to people who prefer each.
Point 3–encouraging a pattern that makes code harder to understand–can be ameliorated by making the compiler throw a warning unless you turn the warning off with a flag. With : Unit =
, the compiler is promising me the same thing as it did before. I’m relying on that. With =
, it could be anything. Having a warning is less regular, but it’s not as big a hit to language complexity as is having a whole separate syntax.
Anyway, I think I’ve said just about everything that I could say on this topic. I’m not outright opposed to this change (despite it negatively affecting me personally), but I do not see that the reasoning presented by the people who advocate the change adequately addresses the potential downsides. It would be nice if it did.
Note that, if : Unit = {
would be removed, you’d have to take extra care with ) = {
that your method actually does return Unit
and not another inferred type, which may lead to many ()
at the end just to signal that.
To be frank, I think requiring : Unit
(unless you’re overriding) might actually be the better choice instead of letting Unit
be an inferrable return type. Or at least a hard warning for using ) = {
with an inferred Unit
return type.
It’s true that I haven’t explicitly listed the drawbacks of such a change (I’m not the person who proposed it either), and it looks like this is my fault since I had assumed obvious all of the disadvantages you’ve mentioned.
To me, being accessible to beginners that are doing their first programming steps with Scala is more important than any of the three drawbacks combined. Making the language more consistent is a good thing, and not only benefits beginners but also experienced developers. Experienced developers are at a privileged position and are more gullible to relearn this than beginners are to get their head around the fact they can do the same thing in two different ways. The cognitive overhead when you learn is already high, let’s not make it higher, even if some people using it will see a slight increase in the verbosity of their code.
It’s true that a warning could go a long way, but that misses the point of Scala 3 being the evolution of Scala 2 without the quirks of its predecessor. Moreover I think that we’re underestimating how many people have already fixed their code not to use procedure syntax because they use IntelliJ (or someone in their team does), as IntelliJ lints whenever it’s used.
That being said, this kind of feedback is the one we’re looking for whenever we ask the Scala community in general about what they think of the changes, so thank you Rex and all the other vehement comments in favor of it. I’ve not tried to be dismissive, but rather explain why I personally think this is a worthwhile and positive change.
I did mention it, but not as clearly as here. And it’s a very good thing that that syntax will be forbidden.
Do you mean that it would be considered a quirk to warn (by default) when def foo = { ... }
has return type Unit
? This becoming commonplace worries me more than any other aspect of the change. Warning seems to me to be new-user-friendly: warning on bad practice should be on by default. Experts can turn it off.
Also as an aside, if we’re trying to make a substantial increase in consistency, has for
been looked at? It’s got all kinds of weirdnesses in it:
- Novel binding of new variables with
=
- Novel
<-
syntax - Postfix
if
evokes apparently redundantwithFilter
method - Silently loses results if you forget
yield
for()
admits;
unlike every other expression surrounded by()
but also allows{}
; does semicolon inference for{}
but not()
- Has a per-element return type, but no easy way to express it
- Thwarts type inference (e.g.
for (a <- List(1); b <- Vector(2)) yield a+b
does not giveSeq(3)
despite not having an obvious type - Is just syntactic sugar, but doesn’t desugar to the idiomatic code that a user would write by hand except in the simplest cases
This is why I was saying that I’d sooner get rid of for
than procedure syntax. I’d rather not get rid of for
either, but have improvements been considered? Actually learning for
in depth is like learning a whole new mini-language.
Man, this has produced a lot of discussion, much more than I certainly would have guessed. I’ll give input from the standpoint of teaching Scala, which I think needs to be considered in language development as it will have an impact on adoption. At first, I wasn’t a fan of this idea, but I now view it as one less special case and a change that makes the language more uniform. This change means that there is a single rule that every non-abstract def is going to have a ‘=’. That uniformity is the type of thing that makes a language easier to learn for novice programmers.
It also simplifies errors. Right now, leaving out a ‘=’ on a method when you need it to produce a value won’t generate an error until the point of usage, which can cause anyone pause, but it is particularly hard for novices who really benefit from error messages that list the line number the actual error occurs on.