While I personally like 3 spaces, I doubt other people would. Most IDEs and editors may allow you to configure 3 spaces, but it may still be headache to switch to 3 spaces for many. I don’t think Scalafmt even allows you to do this (not sure about this, though). Some may even think of it as the worst of both worlds. Scala has a convention of 2 spaces (or sometimes 4), and I really doubt changing the convention is not going to be a very popular decision.
I don’t think Scalafmt even allows you to do this
that should be a solvable issue
Scala has a convention of 2 spaces
It also has mandatory braces for the time being
I really doubt changing the convention is not (sic!) going to be a very popular decision.
I wouldn’t be surprised if the amount of backlash wouldn’t surpass that of this thread
Sure, change is controversial, change affecting literally every single line of your code even more so. But Optional braces is a significant enough change to ask if our current ways of doing things meshes well with it. I looked at some code posted here and felt that visually better separated indentation would improve its readability. On the other hand though, with 4 spaces horizontal whitespace accumulates too quickly.
Might be worth checking what the sentiment is, and if the feedback is more negative than worth dealing with, then just leave it (There’s enough pushback to some changes already that needs being taken into account before 3.0 final, I’m sure reconciliating all the opinions already takes a ton of energy). Idk though… so far I have a like and your reply saying that you would personally like it.
What about using Tab, and let the IDE translate it to whatever number of spaces you see fit ? Indentation would have to be Tab, that would address the stray whitespace problem.
Let me summarize what I took back from feedback on details of the proposal
-
:
at end of line is contentious. It was argued that it does not work well after)
because it’s too close to a return type. There’s also the “false friends” issue with:
in Python. There’s partial overlap in that:
is used in both cases to start the definitions of a class. But Python uses:
everywhere whereas Scala 3 otherwise uses keywords to turn indentation on. Furthermore, if we allow:
for function arguments (i.e. turn -Yindent-colons on), then we have a situation where Scala allows:
but Python doesn’t. -
It’s confusing to have so many keywords that start an indentation region. I’d like to turn this around, actually. In fact, any keyword that must be followed by an expression or template can start an indentation region. There’s no need to keep tab of which keywords do this - if you could have written
... KEYWORD {...}
before, you can write KEYWORD, a new line and indented code now. This principle was not quite true before - two keywords were missing – but it will be true when https://github.com/lampepfl/dotty/pull/10861 is merged. -
Discussions on indentation width. It was argued that code is more legible with higher indentation widths. This would not concern the language definition itself, which allows any number of spaces or tabs, but additional tooling like formatters.
-
We might want to be stricter with enforcing layout. Right now, the scheme is intentionally very liberal in order to accommodate as much existing code as we can without needing a mode switch. Over time we might want to tighten that.
I think this is a good summary. Any thoughts on how to deal with point 1? I know students like the colon, however quite a few experienced Scala developers in this thread clearly do not.
On another note, I think - based on this thread - there seems to be a small majority in favor of optional braces. However I don’t think there is any consensus on how to deal with higher-order functions. IMO every alternative proposed looks worse than just using braces/parentheses. Maybe we are trying to throw out the baby with the bath water here?
I don’t think that’s always true even with this PR, for example (and as discussed before in this thread):
List(1, 2).map(x =>
val y = x + 1
y
)
Does not compile (error expression expected but val found
after the =>
), but adding braces to match indentation does compile:
List(1, 2).map(x => {
val y = x + 1
y
})
Let’s analyze where we would need to replace :
(including code permitted under -Yindent-colons):
- In a class, object, trait, or enum, to start its definitions.
- To demarcate a function argument which is not a closure.
- To demarcate a function argument which is a closure
I believe for (3), “nothing at all will” work, i.e.
xs.map x =>
x * x
is fine. That leaves (1) and (2).
What to do for classes
We might give with
another try. We use it already for givens, so in that sense it would be good for regularity to do the same for classes, traits, objects, and enums. I tried to re-evaluate my experiences from the course. I changed all the slides back to with
and had a close look at them. The pushback against with
might have been due to lack of familiarity. We were all used to writing braces, so having something heavy like with
there looked unfamiliar. :
was less jarring since it was less visible. But I think that’s something that might change once one gets more used to it.
What to do for function arguments
I think we could try do
for function arguments that are not closures
E.g.
logged("file") do
some
code
block
or
inContext(new ForkJoinContext) do
some code
that takes
execution context parameters
or
Signal do
some computation
that should be reified as a signal
I was against do
so far since it feels imperative. And I still think it would not work well for closues. E.g.
xs.exists do x =>
0 < x && x < 10
feels off for me. But if we disregard closure arguments since we have a better solution for them already,
what remains? When would we write {...}
around a multi-line function argument that is not a closure? I believe in most cases it would be an argument evaluated by-name. If this was a complex argument that would be evaluated by value then it’s arguably better to just pull out the argument into a val
. One exception to this is if we want to depend on evaluation order. I.e. in a curried function application, the first argument has some side effect that the second argument relies on. Or vice versa, the first argument reads some state that the second argument modifies. In all of these cases argument evaluation order matters, so we want to see the argument as an expression to be evaluated rather than as something equivalent to a simple value. Something lile evaluate
or eval
would make that clear without the imperative connotation, but do
is probably close enough and feels not as clunky.
Are two keywords too complicated?
Arguably not. with
and do
play different roles. with
introduces a list of definitions whereas do
introduces an expression. Both of them are already used in these respective roles.
Possible Downsides
- Too much “in your face”, “feels to heavy”.
:
is admittedly more quiet from a typographic persepective. do
could still feel wrong in some cases.- have to do some major changes to course materials (that affects mostly me and the people who work with me on this)
Possible Upsides
- The language would feel more uniform.
- We are 100% true to the idea of optional braces. Both
with
anddo
can be followed by either braces or an indented block. - less confusion with Python syntax.
- no confusion with type ascriptions.
So, maybe this makes @lihaoyi and @sjrd each half happy since they were the ones proposing do
and with
here . What do others think?
Sure. But that’s orthogonal. We interpret neither newlines nor indentation insides (...)
. The reason for that is to offer an escape hatch for people who want to use some layout that is not supported by the rules. E.g. in Scala 2, one use of parens would be to avoid semicolon inference in situations like this
( someCondition
|| someOtherCondition
|| someFinalCondition
)
This one is no longer an issue in Scala 3, but there might be other situations where one might want to turn off semicolon or brace inference.
Then I don’t understand why this works:
List(1, 2).map(
x =>
val y = x + 1
y
)
Changing class A:
to class A with
would change my reaction from “this is great!” to “this is awful”.
A colon here makes sense semantically. “This is class A: …”, or “here we define class A as follows: …”, and so on. with
seems like we already have class A and then define something that is “class A, with additionally…”.
And it certainly shouldn’t be necessesary to use two keywords to define a class. Would be better to just go back to braces, then.
On the other hand. If we have:
trait A:
def foo: String
it doesn’t make quite as much semantical sense to reuse the colon to instantiate it anonymously:
val a = new A:
def foo = "foo"
Here, with
might be better:
val a = new A with
def foo = "foo"
as it could be seen as “mixing in” the implementation.
Regarding do
, IMO it’s the worst of all possible choices. It would be enormously confusing in the context of thinking about side-effects. Please avoid at all cost.
I think this makes sense, yes. I think it addresses my objective/technical concerns.
I would add with
on extension
s as well, but I will take your new proposal without that amendment over the status quo.
I do understand your other arguments, but this one is actually not quite right. I get the class already by writing
class A
This introduces a new empty class by itself, not a label for what follows. So, writing
class A with
...
introduces a class A with some additional definitions.
Yes! Let’s go with with
.
And I guess we can do do
.
Though this means the following should also be accepted, for consistency, which still looks weird:
xs.exists do
x =>
0 < x && x < 10
But thankfully that’s not a common existing indentation pattern anyway.
I do wonder if it will always be clear when used in multiple braceless block arguments, though:
foo do
blah
blah
do
bleh
bleh
// i.e.:
foo {
blah
blah
} {
bleh
bleh
}
In the braceless version above, it kind of looks the second do
introduces an independent block.
I see what you mean, and I have no doubt that you are technically correct, of course. But for me as a language user, it still reads like something like class _ extends A with
, even if that is not what it means. I guess what I’m trying to say is that it’s counter-intuitive. And I think the syntax should make sense intuitively rather than just being technically correct.
@kavedaa - I think people have different senses of what is intuitive?
To me, :
is dreadfully unintuitive, as it introduces a type, while with
is quite intuitive because you already can do with Foo
which gives you everything from Foo
. So with { ... }
must give you everything in { ... }
. And if braces are optional, well there you go. It all makes sense. Just takes what you already know and extends it into something that you might have guessed already.
I do agree that do
does not read well. (But I don’t think :
reads any better.)
I was initially opposed to do
, but I think it strikes the right balance between “noticeable enough” and “not too long.” About its imperative nature - Haskell uses do
, and it doesn’t look unnatural (at least not to me).
with
works well enough for me. It is indeed a bit heavy, but it seems consistent and makes intuitive sense to me.
I do think this looks quite good. However I am not sure how this will work with multiple arguments or multiple argument lists. E.g.
// do would be wrong here since it is just an expression
myCollection.foldLeft(initialValue) { elem =>
...
}
// Some result like data type that doesn't allow pattern matching
result.fold(
error => doSomethingWithError(error),
success => doSomethingWithSuccess(success)
)
// Or on a single line when its short
Option(1).fold("", value => value.toString)
In my experience these patterns are quite common, so I think they should be supported in the new scheme as well.
If with
is chosen, what is the plan regarding this case:
Regarding do
, it doesn’t look right in the anonymous class case:
trait KeyListener with
def keyPressed: Unit
def addKeyListener(listener: KeyListener) = ()
addKeyListener do
new KeyListener with
def keyPressed = ()
On second thought, I am having more reservations against do
. do
for me implies that something will be executed now, it’s an imperative after all. But there’s also the case where we just want to build
up a command, i.e. delay code. Signal
construction falls under that, so Signal do
seems off. Or, think of constructing a task graph
task do
val x = ...
val y = ...
f(x, y)
I think that syntax sends exactly the wrong message. We do not “do
” the statements that follow. They are treated as data here, to make up the task being defined.
An alternative might again be “nothing at all”. More precisely, one could allow optional braces after any operator. Here’s an example that comes from the compiler code:
isMatchingApply(tp1)
|| defn.isCompiletimeAppliedType(tycon2.symbol)
&& compareCompiletimeAppliedType(tp2, tp1, fromBelow = true)
|| {
tycon2.info match
case info2: TypeBounds => ...
case info2: ClassInfo => ...
case _ => fourthTry
}
What is a good alternative to the pair of braces? Both do
and :
look strange after ||
. But I don’t think we need anything here! The following code looks fine:
isMatchingApply(tp1)
|| defn.isCompiletimeAppliedType(tycon2.symbol)
&& compareCompiletimeAppliedType(tp2, tp1, fromBelow = true)
||
tycon2.info match
case info2: TypeBounds => ...
case info2: ClassInfo => ...
case _ => fourthTry
So, the rule would simply be that braces can also be omitted after any operator that appears at the end of a line and that is followed by indented code.
To address the case of general function arguments we still need an “apply” operator, which can be defined in a library. F# and some other languages use pipe operators <|
and |>
for this. I believe that’s workable in Scala as well. Alternatively, @
was proposed here and $
is used in Haskell, but I find these more cryptic. So we could define task
as follows:
task <|
val x = ...
val y = ...
f(x, y)
An advantage is that this one can be naturally chained, like any other operator.
someFunction
<|
firstComputation
<|
secondComputation
This would look even nicer if we could create a ligature that merges the <|
to look more like a left triangle. Here’s another example, this time from the Dotty parser:
def constrExpr(): Tree =
if in.isNestedStart then
atSpan(in.offset) <|
inBracesOrIndented <|
val stats = selfInvocation() ::
if isStatSep then { in.nextToken(); blockStatSeq() }
else Nil
Block(stats, unitLiteral)
else
Block(selfInvocation() :: Nil, unitLiteral)
Here’s the same code with braces and parens:
def constrExpr(): Tree =
if in.isNestedStart then
atSpan(in.offset) {
inBracesOrIndented {
val stats = selfInvocation() :: (
if isStatSep then { in.nextToken(); blockStatSeq() }
else Nil
)
Block(stats, unitLiteral)
}
}
else
Block(selfInvocation() :: Nil, unitLiteral)
I think it’s worth investigating this further. To be sure, none of this is for 3.0. The only thing we need to decide now is whether to use :
or with
for classes and similar. But it’s good to know also for this decision that we might not end up using :
for function arguments, after all