I think it tilts the balance towards :
. :
at end of line means {...}
are implied. When this was first discussed the main objection was that then we could not write a function like map
with bound name on the same line as the map
. Now we have a natural way to do so. =>
already opens an indentation region so no new convention is needed.
@odersky Nothing at all for lambdas simply does not work in at least two cases: by name params (or value params presented as block) and lambdas where the parameter list needs to go to the next line. And then there’s methods with multiple lambda parameters (e.g., folds of ADTs).
val tree = atPos(pos) {
val x = ...
x
}
val result = myFairlyLongListExpression.foldLeft(nonTrivialZeroValue) {
(prev, elem) =>
...
}
val result = myOption.fold {
val x = defaultValue
x + 1
} { value =>
val x = value + 1
x * x
}
None of those can be expressed at all if there is nothing to introduce block lambdas.
Using :
does not really solve at least the third case, and perhaps not the first case either.
I am grappling with it. I want to believe that with
or where
would work. But the fact was that I have been there before, changed everything to use them, and they didn’t work very well. And I still try to put my finger on why that was the case. At this point it’s probably too late to go against the evidence we have collected.
Well. I still don’t like this, and I’ve expanded too many times already why I think :
after a term is confusing. To me, it simply doesn’t matter if there’s a newline after the :
. I obviously parse code differently in my head.
I will be able to get used to :
before template bodies, but for terms I think a keyword preferrably do
makes more sense. do
is also the same amount of keystrokes as :
.
Absolutely love the quiet syntax! Braces are just noise to a typographer’s eyes, when horizontal and vertical spacing are sufficient to establish hierarchy. People’s eyes are way more powerful than old parsers.
Using words to denote the transition from a signature to an expression is not the best idea, as they muddy the water between real code and connectives; that is why people designed punctuation marks so that they stand out and are easy to find in one’s peripheral vision. So I hope with
and where
and such will be considered failed experiments. So do I for ..
which is two characters, not one, and is seldom used to denote ranges in situations where using an em-dash is not visually distinct enough.
I find the choice of :
very strange though, and inconsistent with the language which already uses =
as the connective for functions. Using =
to separate template signature from template body would only follow existing rules and open up the opportunity to get rid of the horrendous extends
keyword.
def foo(x: Int): String =
expr
class Foo(x: Int): SuperFoo & SuperBar(x) =
expr
given [A: Ord]: Ord[List[A]] =
expr
Now all one needs to understand the context of a definition is to look at the first word on the line.
@odersky, I would still really like to know (even if you prefer :
) if you see any appeal at all in this:
I don’t see why it would not solve the first case. It’s true that it does not solve the third case. But maybe we don’t need to. I am not a fan of multiple {...}
blocks in a row. I believe it tends to lead to obscure code and is overused. So, maybe not having a convenient brace-less syntax for it is OK. As an example, I would write the Option.fold
example like this:
val result = myOption match
case None =>
val x = defaultValue
x + 1
case Some(value) =>
val x = value + 1
x * x
I would argue that’s much clearer!
EDIT: I see now what you mean with by-name parameters. Yes, if the argument is not a lambda you still need something else. I would argue for :
in that case.
I really get this argument, but I still think the problem that’s very hard to get around is this:
:
works much better in a language like python where it doesn’t have a more important meaning elsewhere.
My personal conclusion is this:
- Code using
:
is harder to read than braces to me. - Code using
where
at least as easy to read as code with braces.
So it were up to me, (of course it’s not) the conclusion from where
or with
not working would simply be that braces are better.
Thanks for the examples, I really love this.
The use of with
and ..
is one of the cleanest and easiest-to-read alternative IMHO.
Though @odersky’s nothing-at-all proposal does make code a bit more quiet visually (unfortunately, it has some limitations, as pointed out by @sjrd).
Compare
broke.foreach ..
case (_, msg) =>
alertables
.map .. a =>
(a, a alert msg)
.collect .. case (a, No(e)) =>
s"Failed to send alert $a\n=========\n$e\n==========\n\n"
.mkString
.tap .. x =>
if (x.nonEmpty) println(x)
with
broke.foreach:
case (_, msg) =>
alertables
.map a =>
(a, a alert msg)
.collect case (a, No(e)) =>
s"Failed to send alert $a\n=========\n$e\n==========\n\n"
.mkString
.tap x =>
if (x.nonEmpty) println(x)
I would call it constructor inference and it would also work for methods:
def foo(x: Int): T = new {...}
, where it would not be incompatible with type inference:
def foo(x: Int) = new {...}
would translate to:
def foo(x: Int) = new Object {...}
It would make with
available for other uses, namely type refinements as proposed by @mbloms:
given abstractDude: Record with Dude with
val name: String
val age: Int
Yeah, I feel very much the same wrt :
. From what I can see in this thread, :
is overloaded so much that it loses a lot of its appeal. I think the multi-line lambda example is the one that stands out most as having nothing to do with it. The name just before the lambda is the only that is needed, isn’t it?
broke.foreach
case (_, msg) =>
expr
… and one-liners are easy to disambiguate with brackets, which is how disambiguation has been done since humans have been disambiguating!
xs map (x => x + 1) filter (x => x > 0)
I converted the example to four spaces, and it ends up looking like this:
case tree: Select =>
val qual = tree.qualifier
val qualSpan = qual.span
val sym = tree.symbol.adjustIfCtorTyparam
registerUseGuarded(qual.symbol.ifExists, sym, selectSpan(tree), tree.source)
if qualSpan.exists && qualSpan.hasLength then
traverse(qual)
case tree: Import =>
if tree.span.exists && tree.span.hasLength then
for sel <- tree.selectors do
val imported = sel.imported.name
if imported != nme.WILDCARD then
for alt <- tree.expr.tpe.member(imported).alternatives do
registerUseGuarded(None, alt.symbol, sel.imported.span, tree.source)
if (alt.symbol.companionClass.exists)
registerUseGuarded(None, alt.symbol.companionClass, sel.imported.span, tree.source)
traverseChildren(tree)
That’s a lot of whitespace. Far too much for me.
This was my spontaneous thought to begin with, but I agree with Martin here. It’s would be weird and unexpected if the type of foo is narrower than T. And it must be, otherwise a lot of stuff breaks.
Can we circle back to why we actually need special syntax for higher order methods? A lambda argument is just an argument, just like a
, "foo"
, Some(42)
. How do we pass arguments to functions or methods? Between (
and )
. Why can’t we simply pass lambda arguments between (
and )
like we do with all other arguments? In Scala 2 we have to use {
}
for multiline lambda’s because ;
inference between parentheses doesn’t work. My guess is this whole discussion wouldn’t even exist if a different design had been picked in Scala 2, as suggested in this post:
The only issue I see is that Scala’s “user-defined control structures” become less native looking. But that seems to be almost inevitable with optional braces. All native syntax now defines the start of the “code block” with different keywords such as then
, do
, with
, :
. So perhaps if we want to keep native-looking user-defined control structures just pick one of those keywords and roll with it:
until(foo < bar) do
println(1)
println(2)
test("println should print to the console") do
println(1)
println(2)
// or
until(foo < bar):
println(1)
println(2)
test("println should print to the console"):
println(1)
println(2)
But in that case I think we should emphasize that the accepted way to pass lambdas and multiline arguments is between regular (
and )
, and this is only for usage in DSLs.
Or alternatively don’t pick any code block starting keyword, and let DSL writers get creative with what’s available. E.g. this is already possible:
scala> object Foo { def update(s: String, f: => Unit) = {println("start"); f; println("end")} }; def test = Foo
// defined object Foo
def test: Foo.type
scala> test("println should print to the console") =
| println(1)
| println(2)
|
start
1
2
end
*** As a side note, may I add that using {
}
instead of (
)
for passing arguments is actually a source of confusion for beginners in scala. I’m 99% sure that virtually all suggestions I have seen here will only worsen the initial confusion.
I passionately agree.
I don’t understand. The type of foo is T. The type of the RHS is narrower than T. IIUC that’s what we call avoidance. I do not see it as a problem. It can even be useful to hide implementation details (encapsulation).
So far, at least two people have said that the Dotty codebase is hard to read. Is there any one who finds the Dotty codebase easy to read?
And for the record, I find most examples posted here hard to read. Maybe using something other than colon :
, something that stands out visually, as a block starter, and using that consistently for every block start, would make it easier. Maybe.
My Python is a bit rusty, but isn’t it the case in Python that a block starts if and only if a line ends with a colon :
?
Multiple ways to start a block, some of which look very similar to other often-used language constructs, that’s pretty much the most efficient way to make a language unreadable.
The problem is that the freshly defined and specialized members defined in the body of the given must be visible on the outside. That’s extremely useful and I’d rather change all the syntax to S-expressions than give that up.
Just a reminder that not all developers are typographers, and some of us find that “noise” to be exceptionally useful for visually picking out boundaries, as horizontal and vertical spacing are not sufficient to establish hierarchy at a glance (at least for me).
Here’s my 2 cents:
- I don’t like that we reuse
:
as much as we have used_
symbol for everything…
I’d like maybe if we usedis
token instead of it, for definitions ofclass
/object
/given
/extension
, for example:
trait A is
def f: Int
class C(x: Int) extends A is
def f = x
given [T](using Ord[T]): Ord[List[T]] is
def compare(x: List[T], y: List[T]) = ???
extension (xs: List[Int]) is
def second: Int = xs.tail.head
-
I don’t understand why
case
s are special cased here?
Is it really needed? It’s just more stuff to learn and parse in my head. -
It’s a bit weird to me that we want to get rid of braces, but then we have end markers… Which are
3+n
-characters longer to type!
And not that useful… I always fold the text to see where it ends in my IDE. -
Scala is (AFAIK) one of rare languages that have multiple parameter lists.
It is not yet clear how to write these in a braceless style… We would need some special keywords for those also… -
Why don’t we leave lambdas and blocks as they are now, I don’t understand the whole fuss about them? Do we really have to do everything right now?
I have to disagree. It would be far better practice to define a new trait/abstract/class for this.