-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Decide on the role of :
in indentation syntax
#7136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
:
in indentation syntax:
in indentation syntax
Regardless of whether we find a solution for (2), I would go for requiring braces for (1) — object, class, and trait definitions — which is what I proposed in the other thread. But let me repeat and strengthen the rationale here:
|
@LPTK I believe that end markers are actually a far superior way to delineate class scopes. And having both braces and end markers would look weird. |
Not sure if this is the correct thread, but: |
At the risk of having to my own devil's advocate in a week or so ... what about using For templates: class Foo extends Bar with SomeTrait with
def x: Int = 42 The class Foo extends Bar with SomeTrait with {
def x: Int = 42
} That syntax makes sense to me, as the contents of the block are added as members of For that, the lexer would introduce an indent if I would actually prefer to simply not have anything to open a template, though. However this might require the parser to feed into the lexer to implement, which is not ideal. Or maybe we can make it work by simply counting opening and closing brackets? That would be nice. For method calls: xs.foreach with
x =>
println(x)
val y = xs.foldLeft(0) with
(prev, x) =>
prev + x
val z = optInt.fold with
println("nope")
with
x => println(x) This syntax had actually been proposed in the original "new implicits" proposals, for implicit parameter lists. So it must have appealed at some point. Except here it is used to pass normal arguments, when they are blocks (by-name params or lambdas). The advantage over
I don't really like what I'm proposing, but I dislike it a lot less than |
Strong agreement. I think |
In #7083 I wrote:
module Main where
import A
import B
main = A.f >> B.f (2) In a partial application such as xs.foreach:I think xs.foreach <| x =>
println(x)
ys.foreach <| y => println(y)
val y = xs.foldLeft(0) <| (prev, x) =>
prev + x If you squint, it looks like begin pattern matchPattern matching is an odd one because val kind = ch match
case ' ' => "space"
case '\t' => "tab"
case _ => s"'$ch'-character" You basically want val kind = ch |>
case ' ' => "space"
case '\t' => "tab"
case _ => s"'$ch'-character" I think we should just allow (1) template definitionsWhat's interesting about template is that it is both a list of members and it's also the body of the constructor. So in that sense it might make sense to keep it the same syntax as block introducer. class Contact(name: String) extends Bar with SomeTrait <|
def x: Int = 42
DB.append(name)
object Contact <|
def apply: Contact = new Contact("") I understand that class Contact(name: String) extends Bar with SomeTrait =
def x: Int = 42
DB.append(name)
object Contact =
def apply: Contact = new Contact("")
Ditto. |
I propose re-using existing keywords wherever possible. The whole point of this exercise is to make the syntax lightweight: having long keywords like Re-using existing keywords can get us surprisingly far. Consider Scalite: package scalite.tutorial package scalite.tutorial
class Point(xc: Int, yc: Int) class Point(xc: Int, yc: Int) {
var x: Int = xc var x: Int = xc
var y: Int = yc var y: Int = yc
def move(dx: Int, dy: Int) = def move(dx: Int, dy: Int) = {
x = x + dx x = x + dx
y = y + dy y = y + dy
}
override def toString() = override def toString() = {
"(" + x + ", " + y + ")" "(" + x + ", " + y + ")"
}
}
object Run object Run {
def apply() = def apply() = {
val pt = new Point(1, 2) val pt = new Point(1, 2)
println(pt) println(pt)
pt.move(10, 10) pt.move(10, 10)
pt.x pt.x
}
} var x = 0 var x = 0
for(i <- 0 until 10) for(i <- 0 until 10) {
val j = i * 2 val j = i * 2
val k = j + 1 val k = j + 1
x += k x += k
}
val list = val list = {
for(i <- 0 to x) yield for(i <- 0 to x) yield {
val j = i + 1 val j = i + 1
i * j i * j
}
}
list.max list.max
// 10100 // 10100 val all = for val all = for {
x <- 0 to 10 x <- 0 to 10
y <- 0 to 10 y <- 0 to 10
if x + y == 10 if x + y == 10
yield } yield {
val z = x * y val z = x * y
z z
}
all.max all.max
// 25 // 25 I think this looks superior to using any delimiter. Note that the above already works in Scala 2.11/12/13, and has worked for 5 years now. We have to use the same whitespace rules for both One subtlety that we have to take care of is providing for higher-order methods. Code like: val foo = bar
.map{x =>
val y = x + 1
y + 1
}
.foreach{ x =>
val y = x + 1
println(y)
} is extremely common with Scala's method-chaining conventions, and I'd want to be able to provide a whitespace-compatible syntax: val foo = bar
.map x =>
val y = x + 1
y + 1
.foreach x =>
val y = x + 1
println(y) In Scalite I commandeered the val xs = 0 until 10 val xs = 0 until 10
val ys = xs.map do val ys = xs.map{
x => x + 1 x => x + 1
}
ys.sum ys.sum
// 55 // 55
val zs = xs.map do val zs = xs.map{
case 1 => 1 case 1 => 1
case 2 => 2 case 2 => 2
case x if x % 2 == 0 => x + 1 case x if x % 2 == 0 => x + 1
case x if x % 2 != 0 => x - 1 case x if x % 2 != 0 => x - 1
}
zs.sum zs.sum
// 45 // 45 val ws = xs.map do x => val ws = xs.map { x =>
val x1 = x + 1 val x1 = x + 1
x1 * x1 x1 * x1
}
ws.sum ws.sum
// 385 // 385 Since we seem to be deprecating do-while loops as I suggested 5 years ago, the For a longer example using what I propose, please take a look at this self-contained JSON parser: I think it would be a good starting point to compare different syntaxes, being meaty enough to really give you a feel of things where trivial 10-line examples do not. The short examples are also illustrative: |
I want to comment on sub-part (2), i.e. what to use for starting an indented argument. I believe that no single keyword would work well in that role.
math.logarithm with
val x = f(y)
x * x I believe
for
x <- xs
y <- ys
do
println(x + y) I believe for pure functions, transpose do
val a: Matrix = ...
val b: Matrix = ...
a * b In fact, the pattern of function application is so general and multi-faceted that no single keyword can do it justice. That's why Dijkstra uses infix point (well, that's taken already in Scala!) and Haskell uses
The As to possible ambiguity with type ascription: I really don't think that's a problem. def f(): T:
return foo would be awkward. But ever since procedure syntax was dropped, Scala does not have syntax where this pattern could occur. The only possible ambiguity is in a type ascription of an expression spanning multiple lines like this: someLongExpression:
someLongType But I have not seen code like this in the wild, and in fact our code base including all tools, all tests, and community build does not have a single instance where this pattern occurs. Why does it not occur? val someId: someLongType =
someLongExpression
someId And, if you really need to write a multi-line ascription, you can aways do: someLongExpression
: someLongType which in fact reads much better. So, in summary, any ambiguity would be extremely rare, and is easily avoided. The other evidence why ambiguities are not a concern is again Python. Python does use One downside of xs.map: x =>
val y = f(x)
g(y) does not work. You have to format it instead as: xs.map:
x =>
val y = f(x)
g(y) I think this is not so bad. In real code the |
To me this formatting is he deal breaker, much more than the ambiguity around type ascriptions. I work with a lot of real code formatted exactly as you describe, and it reads excellently. If the LHS is long, the .map goes on a new line. I have seen no code at all formatted similar to how you propose, and subjectively it looks awful. If it really looked better, people would already be formatting their lambdas like that right now, and they’re not. |
Fair point. Here's a crazy idea for this pattern: use xs.map case x =>
val y = f(x)
g(y)
xs.collect case Some(n) => n
xs.foreach case i => println(s"next: $i") Points in favor:
To make this work we'd have to add one production to
WDYT? |
[Aside: You may have noted that I "ate my own dog food" in the comments above: every single indented section was introduced with |
Honestly, I think we should just use I appreciate the desire to use |
I have strong objections against
case class Point(x: Double, y: Double)
val points: List[Point] = ...
points.map {
case Point(x, y) => ...
} So, |
I'd argue that the vast majority of $ cat foo.rb
def my_map(array)
new_array = []
for element in array
new_array.push yield element
end
new_array
end
result = my_map([1, 2, 3]) do |number|
number * 2
end
puts result.to_s
$ ruby foo.rb
[2, 4, 6] Here it is used exactly as I am proposing for Scala: to delimit a multiline lambda function taking parameters and returning a value, as a replacement for curlies. result = my_map([1, 2, 3]){ |number|
number * 2
} The usage of $ cat foo.rb
result = [1, 2, 3, 4].reduce do |sum, i|
x = i * i
sum + x
end
puts result.to_s
$ ruby foo.rb
30 Honestly I find |
The niche a language occupies is a highly relevant consideration when choosing its syntax. Unless we want Scala to stop trying to occupy the "ultra-powerful type system" niche, where keeps pace with Haskell, I also think precedent is very important when deciding whether a language syntax is a good idea or not. I fully agree that With regards to Finally, I don't think my objections to having a bunch of different block-introductions have been adequately addressed. If we do pick something, I think it should be universal, even if it admits weird stuff. I would rather allow
and insist on
than have to try to intuit which things require |
I think this would look pretty reasonable with a keyword like
Each indented
This would look identical to a while-loop with a multiline condition:
Or if-else
Perhaps even an if-else with a multiline condition:
That would essentially put user-land code syntactically on even footing with the builtin constructs, which seems like exactly what @Ichoran wants |
This seems like a reasonable thing to me. Scala is its own language with its own styles and conventions. I'm pretty sure this whole whitespace experiment was to try and emulate the approachability and widespread appeal of Python. We aren't trying to attract Haskellite's to Scala. That would make "keeping pace with Haskell" a non-goal altogether (though it still is unclear to me how the spelling of keyword affects the power of the type system) |
After thinking a bit more about it I believe the We should also not invent many different constructs that mean the same thing. So, one valid choice would still be: Do nothing. If you want an argument that is a multi-line lambda, use braces, or arrange the lambda vertically, as I had initially proposed. Sure, nobody does it like this now because it costs an extra line. But it might actually lead to clearer code. If we must invent a parens killing operator, I think xs.map with x =>
val y = f(x)
g(y)
xs.collect with Some(n) => n
xs.foreach with i => println(s"next: $i") It looks better than xs.map with f ? You should be able to eta-reduce a lambda
? People will write that sort of code to save a pair of parens! But that's where I think we have made matters worse, not better. One possible choice would be to restrict the type of the right operand of In summary, I am still very much on the fence about all this, so the option of doing nothing for now (i.e. don't introduce a parens killing operator) looks reasonable to me. I have also learned that the two issues of using indentation for arguments and parens killing operators are not necessarily the same, since parens-killing operators will also be used on a single line. |
One idea which might be attractive is to restrict val z =
optInt.fold with
println("nope")
with x =>
val y = f(x)
println(y) The indentation is prompted in one case by People have often wished that If we do this, another question is whether [EDIT:] I think we want to keep it general, since |
Here's two more ideas:
val foo = bar
.map x =>
val y = x + 1
y + 1
.foreach x =>
val y = x + 1
println(y) It seems we would need up to 1 line of lookahead in the lexer/parser, which seems like something that can be afforded. I haven't thought through all possible ambiguities, but it seems to me like with some fiddling this could work. We already do a lenient-parse+post-validation step in parsing lambda argument lists anyway.
val foo = bar
.map fn x =>
val y = x + 1
y + 1
.foreach fn x =>
val y = x + 1
println(y) Short, unambiguous, and precisely meaningful. Sure it would be introducing a new keyword, but I think for such a common operation it is worth is v.s. overloading an existing keyword that doesn't really fit |
I did some exploration, looking at actual usages of sym.baseClasses.exists with ancestor =>
ancestor.hasAnnotation(jsdefn.EnableReflectiveInstantiationAnnot) This is a LOT less clear than the original sym.baseClasses.exists { ancestor =>
ancestor.hasAnnotation(jsdefn.EnableReflectiveInstantiationAnnot)
} (and Can we use just sym.baseClasses.exists: ancestor =>
ancestor.hasAnnotation(jsdefn.EnableReflectiveInstantiationAnnot) would theoretically work since the type in a type ascription cannot be a naked function type without parens around it. But the longer the parameter list gets the harder it would be to parse (for humans). |
If It wouldn't be the first time @ object foo{
identity: Int => Int
println("this is binding `identity` to a self-type of Int followed by a expression " + identity)
}
defined object foo
@ foo
this is binding `identity` to a self-type of Int followed by a expression ammonite.$sess.cmd9$foo$@555856fa
res10: foo.type = ammonite.$sess.cmd9$foo$@555856fa @ object foo{
(identity: Int => Int)
println("this is a type ascribing `Predef.identity` to a function type " + identity)
}
cmd11.sc:3: missing argument list for method identity in object Predef
println("but this one is " + identity) @ object foo{
val x = {identity: Int => Int}
println("this is binding `identity` to an Int argument of a lambda which returns the Int companion " + x(1))
}
defined object foo
@ foo
this is binding `identity` to an Int argument of a lambda which returns the Int companion object scala.Int While not ideal, it seems to have caused little enough unhappiness in the past, so I wouldn't mind pushing the envelope a little bit and overloading |
I appreciate the effort, but don't think the results from any proposal are particularly visually pleasing or sufficiently general. I'm somewhat puzzled about why people aren't more concerned about the generality/simplicity. I think we should have a good story about how to select when and where to put a brace-killer. I don't think memorizing a dozen or more cases is a good story. This suggests to me that the solution is that brace-killing happens always. Maybe you require no syntax:
Or maybe you do:
but I don't think it works to have to pick and choose the cases. I think even Python doesn't make you remember a pile of cases. From a random example on the web:
Do you see that Yes, it's just (redundant) punctuation. But punctuation is important:
I think the discussion about |
@Ichoran Putting |
little joke These : used as begin of a indentation looks like an alias of a { without a corresponding }. You can show this with a simple \{ or {{ at the end of the line. The : should be separated with a space (xyz : not xyz:) for better reading. object IndentWidth {{
private val spaces = IArray.tabulate(MaxCached + 1) {{
new Run(' ', _)
end IndentWidth |
@odersky - I don't understand what you mean--surely having a brace-free style is making a different dialect? (Just like Anyway, we would only require In 2.12/13, |
I should describe my idea better. The first time I saw that, I was a bit confused. The other suggestions, like do or with, do not feel better either. That's why I wanted to propose a block opener for the brace-free style, which is similar to the normal style. My suggestion here is the double {{ . (I know { on a brace-free style, but we also use () ) Both the single { and the double {{ carry the same message, start a new block, but the latter with brace-free style. The double {{ does not collide with other constructs either. |
For partial application, I guess some languages (Elm?) do use elements.foreach \elem =>
doSomeStuffWith(elem)
println(elem)
elements.foldLeft(0) \elem, agg =>
elem.toInt + agg I did get use to it, some I'm obvisouly biased, but a bit curious if it could be a potential candidate. |
@krakel - I like my parentheses, brackets, and braces balanced. (Angled brackets too, in languages that use them as brackets.) I suspect there are enough people with a visceral aversion to imbalanced parens etc. that this is not feasible. @AndreVanDelft - Memorizing a bunch of superfluous method argument names isn't fun. I would reject anything that requires it. @odersky - Did you still want to have optional braces? If yes, you have to consider how all of these things work with braces plus the block opener: logging("stdout") fn {
xs.map fn {
case a => ???
case b => ???
}.filter fn { x =>
x >= 0
&& x < limit
}.exists fn { x =>
val y = x * x
y >= limit
}.fold(zero) fn { (x, y) =>
val z = x + y
z * z
}
}
logging("stdout") <| {
xs.map <| {
case a => ???
case b => ???
}.filter <| { x =>
x >= 0
&& x < limit
}.exists <| { x =>
val y = x * x
y >= limit
}.fold(zero) <| { (x, y) =>
val z = x + y
z * z
}
}
logging("stdout"): {
xs.map: {
case a => ???
case b => ???
}.filter: { x =>
x >= 0
&& x < limit
}.exists: { x =>
val y = x * x
y >= limit
}.fold(zero): { (x, y) =>
val z = x + y
z * z
}
}
logging("stdout") @ {
xs.map @ {
case a => ???
case b => ???
}.filter @ { x =>
x >= 0
&& x < limit
}.exists @ { x =>
val y = x * x
y >= limit
}.fold(zero) @ { (x, y) =>
val z = x + y
z * z
}
} None of these look very good to me, but to my eye logging("stdout"){
xs.
map{
case a => ???
case b => ???
}.
filter{ x =>
x >= 0 &&
x < limit
}.
exists{ x =>
val y = x * x
y >= limit
}.
fold(zero){ (x, y) =>
val z = x + y
z * z
}
} I find that with this formatting, the visual weight of the braces is greatly reduced. By using dots at end of lines instead of the beginning, the method names leap out even more, making the control flow easier to inspect. But with brace-elision, the dots are absolutely required at the beginning or the meaning changes. So I'm not entirely convinced the advantages of going brace-free are quite as big as suggested. I think better style choices can help with existing syntax. |
Martin:
I understand. More layout exploitations are possible. I gave some 'Wild Ideas' in my Lambda Days talk (PDF). Some of these exploitations will conflict with others. I'd propose to make consistent dialects available through |
The problem noted by @jxtps with val a = x *
y + z only affects lines ending with a binary operator, right? So we could save the existing behaviour ( That allows this, which looks natural to me: if a < b &&
b < c
1
else
2 .. provided lines 2 and 3 are indented differently. The compiler should warn you when they aren't (i.e. when a line continuation line is followed by a block opening line with the same indentation level). |
Then with logging("stdout")
xs.map
case 0 => 1
case x => x
.filter : x =>
x >= 0
&& x < limit
.fold
zero
: (x, y) =>
val z = x + y
z * z Note that a space before |
The merged PRs #7185 and #7235 implement the following scheme: Braces are optional for correctly indented code
A This means that we defer until later the question whether to adopt |
After having worked with the new rules (without def foo =
abc The def foo =
println("abc" ++
"xyz") and def foo =
println("abc")
"xyz" are equally clear and braces are needed in neither case. So, I find making braces optional here is a clear win because it improves writability and readability at the same time. With class and object headers it's different. Here when I see class Rational(x: Int, y: Int)
/** The numerator */
def numer = x
/** The denominator */
def denom = y I am less sure how to read that. Are By contrast, braces help here. When I see class Rational(x: Int, y: Int) {
...
} I know that the next definitions Possible solutions:
|
When fiddling with 0.19 I immediately made that mistake on a few class defs without the colon, especially for case classes where I tend to put parameters on their own line. It was a bit hard to read without the colon as I was not using an case class Foo(
x: Int,
s: String,
// more params
// ...
): // <-- this colon was very helpful when reading the definition
def someMethod: String = ??? |
Do you see in the long term brace-less syntax as an alternative or an replacement? |
It seems like the primary difference between the class/object case and the method case is less that something must follow in the method case, and more that even short classes/objects typically have multiple methods/fields and there's vertical white space between them. If we were to consider a method with a longer body: def foo(i:Int, y:Int):Int =
println("Welcome to foo!")
val a = i * y
println("A is: " + a)
for (val b <- 0 until a) print(".")
println("Goodbye!")
a+2 then it's no longer any more or less clear what's part of the body vs what's part of the enclosing scope compared to classes/objects, right?
Sure, the first line of a method may be protected by "something must follow", but the subsequent lines are not, right? In my Python experience (with 4 spaces per indent which was the editor's default) there's never been any ambiguity whatsoever about these things. Copy-paste is a little futzy, but I've never found myself in a situation where I wonder what scope a given line of code is in. For the cases where it gets hairy - really long methods / classes - any braces or Instead, I would consider going draconian: if you're going to use indentation-based syntax, the indentation shall be 4 spaces per level, no tabs, no exceptions. See PEP-8 - I'm guessing the Python folks have hashed this out a lot more over the years than we have ;) |
It does not work that way for me. Visually, the first line after the But with classes it's different. Here, I have to check whether something is indented relative to the previous line. That's much harder, and suffers from off-by-one-space errors. Going to 4 spaces might help but it's not something we can require. |
One downside of using given c: C:
def f() = ... When I originally proposed The alternative of using trait Monoid[T] extends SemiGroup[T] with
def unit: T
given Monoid[String] with
def (x: String) combine (y: String): String = x.concat(y)
def unit: String = "" I am less keen on using |
I really wish all the time being invested in this would be saved for after some more urgent things, for instance getting TASTy to the point where it can be used in Scala 2.x. |
Definitely agree that it would be good to move forward on TASTy! I don't think we can prevent discussions and experiments around syntax (whatever our opinion), but I'm positive that PRs are welcome :-) |
@nafg @TheElectronWill This is being worked on at the Scala Center: TASTy Reader for Scala 2 |
@odersky - I really like the trait A { def a: A }
trait B { def b: B}
trait C extends A with B
trait A { def a: A }
trait C extends A with
def b: B |
There is an argument to be made that the braces vs indentation thing should largely be handled by the editor. To that end I have filed a feature request with IntelliJ: https://youtrack.jetbrains.net/issue/IDEA-224361 - you may want to consider filing similar requests with your favorite editor, or voting for / promoting such features where applicable / possible. |
You can change the syntax coloring to make braces a really light color (or
dark depending on your background). And intellij already inserts closing
braces for you. What more can they do exactly?
If you want to go in this direction the broader issue is that sources are
text files. But once TASTy is finalized someone can make a GUI editor for
it that looks like a text editor (it would need to typecheck on the fly) or
an untyped analogue.
…On Mon, Oct 7, 2019, 7:08 PM jxtps ***@***.***> wrote:
There is an argument to be made that the braces vs indentation thing
should largely be handled by the editor.
To that end I have filed a feature request with IntelliJ:
https://youtrack.jetbrains.net/issue/IDEA-224361 - you may want to
consider filing similar requests with your favorite editor, or voting for /
promoting such features where applicable / possible.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#7136>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAAYAUGX3N6LJSSOJNJYIY3QNO6OBANCNFSM4ISLCKTQ>
.
|
Why not regularize the definition syntax for object/classes/traits to match the method definition? class Rational(x: Int, y: Int) =
/** The numerator */
def numer = x
...
object Foo =
....
trait Bar(x: Int, y: String) =
...
end trait
given c: C =
def f() = ...
case class Qux(
x: Int,
s: String,
// more params
// ...
) =
def someMethod: String = ???
That would avoid the overloaded meaning of the colon. |
@esarbe This has already been offered during the discussions on indentation-based syntax. IIRC, the argument against doing this was the following:
|
Ah, interesting. I never considered the |
Well, |
In a sense, a class is a subtype of its template's structural type. So if
you really want to go down this rabbit hole... how about <:
class Rational(x: Int, y: Int) <:
/** The numerator */
def numer = x
...
…On Wed, Oct 9, 2019 at 9:24 AM Guillaume Raffin ***@***.***> wrote:
Well, == still means equal and x = 2 still means assign 2 to variable x,
but there still is a semantic issue with class A = something
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#7136>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAAYAUHZJCNFZCJUQ2FA533QNXLP5ANCNFSM4ISLCKTQ>
.
|
I'd like to +1 on the strategy of regularising brace-formatted scala code and then allowing braces to be dropped. I think this makes the sales pitch (/impact assessment) less fraught. Braces seems somewhat arbitrarily optional at the moment (in the sense that, eg., in Java, every class definition requires them). There's a strong case for saying "always optional", if that can be achieved with minimal cognitive overhead. Its quite common in C to drop braces wherever possible (whether advisable or not) -- I think the appetite for reduced line noise is pretty widespread. It seems "with" does make sense for bodies/definitions (,...) as there does feel something similar in intention when saying "with Trait" and "with {body}" -- ie., with Trait (qua mixin) serves as a kind of syntactical abbreviation for the in-place definition. Is there a case for the colon and |
A status report: Current master allows to drop braces around templates after
Usecase (1) is an alternative to
I find the double use of
Usecase (2) is still open. Currently
Enabling this syntax is almost possible. The only clash is with an explicit self type declaration at the start of a class or trait. E.g.
Here, My current and still provisional tendency is to do nothing about usecase (2) and to retire the -Yindent-colons option for now. That way, we can evaluate the other changes first before taking the next step. I believe a parens killing operator could still be an interesting option at some point, but I see less urgency to put it in Scala 3.0. |
Closing since it looks there won't be anything standardized for 3.0. We can of course get back to it at some later point. |
The meaning of
:
for significant indentation is more contentious than other aspects. We should come up with a crisp and intuitive definition where:
is allowed.One way I'd like to frame significant indentation in Scala 3, is that braces are optional, analogously to how semicolons are optional. But what does that mean? If we disregard
:
it means:The second condition is important, since otherwise any deviation from straight indentation would be significant, which would be dangerous and a strain on the eyes. But with that condition it's straightforward: If some subordinate code is required, either that code follows on the same line, or it is on a following line, in which case it should be indented. Braces are then optional, they are not needed to decide code structure. In the following I assume this part as given.
So that leaves the places where some code is not required but we still like to insert braces. There are two reasonable use cases for this:
xs.foreach: ...
native syntax. If native syntax allows to drop braces, there should be a way for library-defined
syntax to do the same.
Possible schemes
The current scheme relies on colons at end of lines that can be inserted in a large number of situations. There are several possible alternative approaches I can see:
Don't do it. Insist on braces for (1) and (2).
Split (1) and (2). Make indentation significant after class, object, etc headers without requiring a semicolon. This has the problem that it is not immediately clear whether we define an empty template or not. E.g. in
it is hard to see whether the second
object
is on the same level as the first or subordinate. But semantically it makes a big difference. So a system like that would be fragile. By contrast, a mandatory:
would make it clear. Then the version above would be two objects on the same level and to get a subordinate member object Y you'd write instead:So I actually quite like the colon in this role.
Split (1) and (2) but require another keyword to start template definitions after class, object, etc headers. @eed3si9n suggested
where
. It's a possibility, but again I do like:
at this point. It reads better IMO (andwhere
might be a useful keyword to have elsewhere, e.g. in a future Scala with predicate refinement types).Keep
:
(or whatever) to start templates but introduce a general "parens-killing" operator such as$
in Haskell with a mandatory RHS argument. If that occurred at the end of a line, braces would be optional according to our ground rules.I fear that a general operator like that would lead to code that was hard to read for non-experts. I personally find
$
did a lot more harm than good to the readability of Haskell code.Restrict the role of
:
to the two use cases above. I.e. allow a colon if]
or closing parenthesis)
.This way, we would avoid the confusing line noise that can happen if we allow
:
more freely.My personal tendency would be to go for that last option.
The text was updated successfully, but these errors were encountered: