splitting this off from #ideas > insignificant whitespace because that thread is huge! :big_smile:
I found a design I really like that removes significant indentation from Roc's syntax in a way that feels mostly like a visual tweak to me instead of something really invasive
here's a diff on the roc-realworld code base
I recommend looking at individual files too, such as main.roc or Article.roc
the way this syntax works compared to today is:
{ ... } syntax for block expressions that essentially work like def-expressions do today, except with explicit delimiters. They let you have multiple statements/expressions (that end in a final expression, which the whole block evaluates to) just like you can in Rust with { ... }.{ x } (which is the only ambiguous one) should actually always be interpreted as a block, because why would anyone want to create a 1-field record? (If really absolutely desired, we could allow { x, } but I don't even think that's really necessary.)this design is 100% indentation-agnostic, which means copy/paste from anywhere can Just Work, and also large language models should work fine with them (at work, I've seen them struggle to get indentation right when suggesting changes to an existing code base)
compared to the do and end designs we'd discussed in #ideas > insignificant whitespace, the {and } delimiters are a lot less noisy. As a bonus they work for if and when and for and while and are more mainstream.
I also like that the { and } are only a character apiece, and can very often be omitted without sacrificing any clarity or losing any of the above benefits. It makes the change feel more like a tweak to me than an overhaul.
I know there was a ton of iteration on the other thread, but this is the design I've ended up liking the most. I'm curious what others think of it!
This would avoid commas in weird places, like on the last line of a function in a record:
logger = {
init: |config|
env_level =
config.env.get("log_level") ?? "warn"
set_up_logs(path, env_level),
}
Now the comma is after a brace, which is much easier to find
logger = {
init: |config| {
env_level =
config.env.get("log_level") ?? "warn"
set_up_logs(path, env_level)
},
}
Coming from using 90+% {}-delimited languages thru-out my career (C/C++, Java, Javascript, Typescript, Rust), this definitely makes me feel more comfortable
Anyway, I think using braces aren't as nice looking as today's brace-less syntax, but they are more readable because I can scan scope easily and more consistently
It also makes parsing a whole lot easier
It's non-intrusive enough that I'd be okay with this
I think my primary concern here would be alienating other members of the community
I'm definitely curious to hear what community members think here! :smiley:
I'm overall for it. I think braces are standard and consistent while being pretty visually minimal (especially if you can avoid them for simple cases). I feel like removal of WSA was a much bigger change and this is just minor and not a big deal either way.
I personally am 100% in on auto formatting that everyone should use. I think braces make it easier for that to be consistent.
I like this syntax.
The lambda expressions have just one expression for thier body seems new... but it feels familiar in practice.
I really like changes in the diff.
when -> match - new people no longer need to think about difference between when and if, anyone who's heard of pattern matching will immiedietly recognize what's going on
I also really like braces to limit scopes. It helps both - visually but also when editing.
There's something that I just thought of, sometimes I want to delete whole function body and start writing it again from scratch, using braces instead of whitespace allows me to do that using modal editors (mi{d in helix, di{ in vim).
With all the latest changes and braces Roc is more similar to mainstream languages (TS/Rust) in a good way as if it was taking good ideas from all of them. I know that usually comparing something to TS is used as an insult but I promise that I mean no disrespect here xD
I think "good, and familiar" is good, and "bad, but familiar" is bad
so as long as it seems good, familiar on top of that is even better!
The expression
{ x }(which is the only ambiguous one) should actually always be interpreted as a block, because why would anyone want to create a 1-field record? (If really absolutely desired, we could allow{ x, }but I don't even think that's really necessary.)
One possible use case for a 1-field record is when you expect the a return type of a function to evolve. If you have a function that initially just needs to return some data { x: Str }, but you know that soon you're going to expand the function to also return { y }. You can do this without single field records by first having fun : Str -> Str, then updating it to fun : Str -> { x: Str, y: Str }, but then you have to update all the callsites because the return type is different (was Str but now is a record). But if it was a record from the start, it's a non-breaking change to add y to the return type.
That said, I like the syntax!
Personally I'd use phrase TS syntax done right. Less noisy (no parens around if conditions, no semicolons when they provide no benefits, match instead of if/else chains or switch statements requiring 'break'), more feature rich and information dense.
Definitely "good and familiar", even "cozy" :-)
Do we still want to do some amount of indent-sensitivity?
How would you expect we distinguish these, if at all?
a = |x| {
x!()-x!()
}
b = |x| {
x!() -x!()
}
c = |x| {
x!()
-x!()
}
d = |x| {
x!()
-x!()
}
e = |x| {
x!()
- x!() # note the space!
}
Which of those are one statement with a subtract op vs two statements one of which is negated?
(I specifically used - because that's the only op I know of that could be either unary or binary; I don't _think_ there are other ambiguous cases right now, but I also don't know if I could completely rule it out)
(I guess this also applies to the do/end proposal as well)
Joshua Warner said:
Do we still want to do some amount of indent-sensitivity?
How would you expect we distinguish these, if at all?a = |x| { x!()-x!() } b = |x| { x!() -x!() } c = |x| { x!() -x!() } d = |x| { x!() -x!() } e = |x| { x!() - x!() # note the space! }Which of those are one statement with a subtract op vs two statements one of which is negated?
I think newline-sensitivity is fine
indentation is what that causes problems
concretely, so I'd say:
x!()-x!()
equivalent to x!() - x!()
x!() -x!()
equivalent to:
x!()
-x!()
x!()
-x!()
also equivalent to the above
so only the first one would be different
- has always been full of edge cases :stuck_out_tongue:
So how would I write something like this?
debugged_negative_epoch = ||
Stdout.line!("Getting the negative epoch")
-get_epoch!()
}
Wouldn't that be interpreted as line! - get_epoch!
nah that's what I mean about being newline-sensitive being fine
also in that case it's prob enough to just see that the dash is touching on one side only
Unary ops have to touch
Same with trailing ?
It has to be attached to the expression
Well for ? a space before means something different
And also Richard, I think that actually |x| { x } could be treated as unambiguously as a single field record, as a block is expecting more than a single expression. Don't know what's more surprising:
|x| {
x
}
Being a single (punned) field record, or a block that return the value of x.
So if you had this:
debugged_negative_epoch = ||
Stdout.line!("Getting the negative epoch")
- get_epoch!()
}
That would compile and be treated as Stdout.line!("Getting the negative epoch") - get_epoch!() (one expression)
i.e. the user didn't realize there needs to be no space between the negative and the value - how do we make sure that's not a super surprising experience?
This isn't a _new_ problem, since exactly the same thing can happen currently - but I imagine folks coming from '{}' languages may not realize the spaces around a unary - are important.
I'd be tempted to say something like: you _should_ always indent binary operators that continue from the previous line, and we warn if you don't.
I hate to say this, but I didn't realize that the unary operator DID NOT have to touch in other languages, as I've never NOT done that, or inadvertently done that in a way that was surprising in 27 years of coding
huh
:chili_pepper: Maybe you did do that at one point but never noticed because the compiler didn't yell at you and in that language it meant the same thing anyway.
But yeah, fair, I certainly can't refute that
It's possible!
Wouldn't you immediately in the case above get a type error "Can't subtract {}"
And then you would get a problem and move on...?
yeah I think it's 100% fine to assume people only want unary op if it's touching
I think we probably ought to have the formatter indent that line to make things clear tho
totally! :+1:
I agree with that!
I think in JS I may have been saved by ASI (automatic semicolon insertion) It's incredibly hard for me to do what you did above in JS
I actually like the idea of having the parser detect semicolons and then we have the formatter discard them for you
then we could give a warning like "Roc doesn't have semicolons. You're free!"
We could do what Go does and treat them as whitespace
Or, at least end-of-line whitespace
sure, and then have the formatter replace them with an actual newline
Yep, have it (collapsed with a following newline if it exists) into a newline token
That should reliably get what you want in the formatter
that's trivial actually
Man, semicolons, braces, and PNC. What is this C/Algol descendant? :rofl:
I think this should help out tree-sitter a lot :big_smile:
Definitely
Also, we can now have a context-free grammar
hm, is that true?
Which will make all manner of tooling easier
I think so...
(maybe it is! I hadn't actually thought about it)
WSS is what makes context-free impossible typically
because the indent level is context you have to track
right, but I guess the question is - are there other remaining blockers?
like maybe something related to string interpolation (but probably not that one)
yeah maybe it is!
I'm no expert here, maybe @Joshua Warner can chime in. But I don't think so
Technically multiline strings need a non-context-free-grammar in order to understand the semantics correctly, but not just for building a valid-enough syntax tree
what if they were just "line begins with """ and ends with newline, and indentation doesn't matter"?
and then as many consecutive lines of those as you have, they all go together in one string literal
I think that does it, yeah
so theoretically you could do like:
my_str =
"""foo
"""bar
"""baz
and then the formatter takes care of lining them up for you
that seems like the error-tolerant way to do that design anyway :big_smile:
I like it
12 messages were moved from this topic to #ideas > multiline string syntax by Luke Boswell.
Does braces mean roc is entering its awkward tween years?
it is almost zero point oneteen years old!
("almost" doing a lot of work there)
This looks really nice. My only gripe is that now there are multiple ways to write lambdas. In languages with optional braces around lambdas like this, I always want my lambda to be without braces if possible and then end up a tiny bit frustrated when I need to add braces later to move to multiple lines.
I will happily make that sacrifice for the familiarity boost and getting rid of whitespace significance though :smiley:
I know this isn't quite what you're getting at Isaac, but it makes me wonder, maybe we can make braces get formatted away if there's only a single expression inside to force a single way of writing these
This probably would only make sense for lambdas
Since if and else would look weird if we did it to them
I do like the consistency, but that would be annoying if you know you need to write a multiline lambda, but you've just written one line so far, and then it gets formatted away
True
If you control when the formatter runs, it works, but if you have it run on auto save it'll get annoying
yeah I think if and else and match should all require the braces
Don't we want to support one-line if and else?
I understand if and else wanting to both have braces if one of them does
if foo { 1 } else {2}
That said, I wouldn't be sad if we stripped away braces for expressions below some complexity level
e.g. constants and variables are fine to have outside of braces. Everything else probably deserves braces. (arguable!)
eh I don't think the formatter needs to mess with removing or adding braces
Actually constants could be a bit confusing with if, e.g. if foo 1 else 1 has the condition and then branch jumbled together. Technically not ambiguous in that case, but confusing.
we can see if there's demand for formatter intervention (e.g. because people are arguing over style preferences or something and want an authoritative resolution so they can stop arguing), but it doesn't seem obviously necessary, and might be annoying
Yeah...
Responding to the if else brace example before Zulip loaded the messages
Loving this. It's exactly what I would do, and I know that because I recently went through the exercise of making a simplified Roc-ish syntax without indentation for EYG (mentioned in #off topic). Also liking when going to match.
By biggest worry about end keyword was needing to keep track of which constructs need them. Whereas it's easy to understand that for every { there is a }.
"indentation as nesting" is nice for reading (though IMO not a clear win overall) and maybe someday that becomes more common as an editor-view so we can see look at shorter code without messing up copy-paste
Traditionally, you need some kind of delimiter after the condition within an if, be that ) (like in C, javascript), or { like in rust. That makes parsing easy. I can't think of an example that would cause a problem in current Roc though.
if ident -3 else 3 comes to mind, but that wouldn't be a problem in Roc because of how the unary operator works. I still think we should stick with the braces for ifs, just so that people don't have to think about it. It's more consistent, an extra } at the EoL isn't that bad.
Also, this match syntax would allow piping into match, if that's something we want later.
inverted_directions = directions.map(.match {
Left => Right
Right => Left
})
Overall, I too like this change.
Overall, seems good.
What if the formatter always added braces if the body was on a different line, and always removed braces from same-line functions and ifs?
I'd prefer the status quo (to no surprise). However, I think this is a better solution than do/end. Even though I would much prefer to not have to type {}, and have the LOC growth, I'm used to it from from Rust. I wonder what Roc would look like if the compiler was written in Haskell? Most of the changes seem Rust-inspired (?, ||, {}, discussion about parens in types which is like <>, semicolon sugar for {} = ). I really do think that these suggestions and our tendency to agree with them stems from everyone's daily Rust/Zig usage.
There's probably something to the tooling ideas, like Sky's above. Perhaps that's a best of both worlds? I don't have to type all those braces, they just appear.
Overall though, I think this may very well be the best decision for Roc. Everyone seems on board, it is familiar to a lot of developers, and it solves some issues (which to me are not huge, like copy/paste, but issues nonetheless). It's uncomfortable for me to be this contrarian here, as I generally just want the project to succeed and move forward. That's more important to me than exactly what syntax it'll have.
My attraction to Roc is: "ML syntax, error-handling from heaven, can be used for anything, and it's fast (iteration + runtime)". That really feels like "a language for life" to me. This would kill the first point and of course that stings a bit. The others are still true though, and I get to start a new project! www.arewerustyet.com :joy:
Niclas Ahden said:
I wonder what Roc would look like if the compiler was written in Haskell? Most of the changes seem Rust-inspired (
?,||,{}, discussion about parens in types which is like<>, semicolon sugar for{} =). I really do think that these suggestions and our tendency to agree with them stems from everyone's daily Rust/Zig usage.
I actually think it's more that we really explored the full range of options in that other thread - we started with the most Haskellish syntax, talked about do ... end from Ruby, talked about braces...really the only widely-used option we didn't seriously discuss is S-Expressions, and I don't think there was a need to discuss that one
to me, the main advantage of { compared to do .. end is that it's less visually noisy, and the main advantage compared to significant indentation is that it doesn't bring the drawbacks of significant indentation that motivated the other thread
I think ( instead of { would have all those same characteristics, but of course { is way more mainstream of a choice to use than ( and they're both equally concise, so { makes more sense to me as a choice because its weirdness budget cost is dramatically lower
so overall, I think it's more of a "this had the best tradeoffs" than "this looks Rusty/Ziggy" - although it's fair to observe that they do similar things! :big_smile:
btw I can't emphasize enough how much I appreciate your being up-front about your preferences but being on board with this even though it's not your first choice...if there's one thing I've learned from all our syntax discussions over the years, it's that every decision will always have some amount of support and some amount of opposition, and full consensus is never going to happen :sweat_smile:
the amount of consensus in this thread is definitely the most we've had on the subject, and I really appreciate your going with it! :heart:
I'm surprised
Reading these syntax discussions reminds me of Reason ML. Reason uses braces and the docs include a specific callout for the single field record edge case
https://reasonml.github.io/docs/en/record#single-field-records
I like reason's resolution
I haven’t kept up with Zulip, but I just wanted to mention I’d really like Roc to get braces. I think they just make code easier to navigate (with things like % in vim) and easier to edit in the era of formatters.
I think we can do a decent amount of "parser doesn't require braces and formatter adds them" - e.g. parser accepts if a b else c and the formatter changes it if a { b } else { c }
Richard Feldman said:
I think we can do a decent amount of "parser doesn't require braces and formatter adds them" - e.g. parser accepts
if a b else cand the formatter changes itif a { b } else { c }
I think we'll have to talk about this.
So at the end of the day - as I'm implementing this in the Parser this weekend - is that braces delimit blocks and whitespace (read: indentation) significance goes away.
Where a block replaces the concept of Defs exprs
New lines are still important tho
So any def in a lambda body, or any "statement" like a null def, has to be in a block
Of course
Every statement in a block must be followed by a newline
So inside of a block there are three broad classes of statements:
The name for #2 above is up for debate
For the purpose of parsing and formatting 2 and 3 are the same thing I think
Yes, actually #2 and #3 no longer have to be separate
We will only have to report a problem if a block ends in a def
hm, did it end up being true that newlines are different from spaces?
I'm ok if so, I just thought the parser could treat them as equivalent with no problem
You mean that:
|a| { b = 1 c = 2 d = 4 a + b + c + d }
Would be accepted?
and more significantly, if I'm right that they can be considered equivalent, I think that makes the grammar both simpler to implement and also simpler to understand
yeah exactly
like obviously the formatter should rewrite that to have newlines
but I think it's unambiguous for the parser to accept it
Trying to think of the places where that is difficult....
There are some key places where we absolutely have to still pay attention to new lines (unless we make further syntax changes)
You are probably right that we could
I had a post about this, one sec
Joshua Warner said:
There are some key places where we absolutely have to still pay attention to new lines (unless we make further syntax changes)
such as?
Should have posted that in this channel, come to think of it
I wish those links worked in Zulip's mobile app :sweat_smile:
Oof
can you quote it?
There are a couple of interesting places where, with indent insensitive parsing, we need to disallow line breaks at that point in the expression.
For example, you can't put a line break after the function and before the parentheses. and you can't put a line break between the ? operator and it's right operand.
Some motivating examples:
# If we allowed this:
foo
(1, 2) # user intends these to be args of a funtion
# ... then we'd have trouble with this:
y = 1 + x
(1, 2) # returning a tuple
# If we allowed this:
text = File.readUtf8!(path) ?
ErrorReadingConfig # user intend this to be a binary '?'
# ... then we'd have trouble with this:
text = File.readUtf8!(path)? # Unary '?' is intended
MyTag value = give_me_a_tagged_return_value(text)
ah! So in these cases, I think it's more about whether any whitespace at all is allowed than newlines vs spaces. Specifically, I think:
foo(1,2) is a valid function call, but foo (1, 2) is not a valid function call (and neither is putting any other whitespace there instead of a space)? means something different if it's preceded by whitespace vs if it isn'tAhhh interesting
I still think we should warn if you don’t have a new line between statements in a block
yeah I think warning is fine :thumbs_up:
but I do like the idea of all whitespace being interchangeable if we can get away with it
I think it makes it a bit easier to teach if you never have to think about what particular type of whitespace you're dealing with, but also I think it helps simplify the mental model
like I think if you realize that this is valid:
|a, b| { c = a + b d = c + 1 d * 2 }`
...even if you would never write it that way, I think it helps in understanding where the boundaries are
I did just realize that needs braces
because you only get to have whitespace-separated expressions and statements inside braces
We can easily add an assert in tests that if you replace all new lines with spaces, that it still parses to the same thing.
this is also why I like allowing (but warning and reformatting) if a b else c
because it can be taught that blocks ({ ... }) can be used anywhere an expression is accepted
and then we still have the property that else if is not special - it's just if a b else if c else d
and we choose to require braces as a matter of formatting style to make things read better
Does this unlock anything useful via alternative formattings? Not necessarily by third parties or for source code at rest, but perhaps... shrinking hints in CLI output? shrinking types in tooltips? Those sorts of sneaky spots
I can't think of any reasons we'd want to format it like that :sweat_smile:
If you have short variables or literals, if foo 1 else 2 has less visual noise than if foo { 1 } else { 2 }
It does look kinda weird tho...
I thought about proposing preserving the then for that reason, but if we're really always going to multiline it then it's not worth it.
In the parser now:
test {
try moduleFmtsSame(
\\app [main!] { pf: platform "../basic-cli/platform.roc" }
\\
\\import pf.Stdout
\\
\\main! = Stdout.line!("Hello, world!")
);
try moduleFmtsSame(
\\app [main!] { pf: platform "../basic-cli/platform.roc" }
\\
\\import pf.Stdout
\\
\\main! = {
\\ world = "World"
\\ Stdout.line!("Hello, world!")
\\}
);
try moduleFmtsTo(
\\app [main!] { pf: platform "../basic-cli/platform.roc" }
\\
\\import pf.Stdout
\\
\\main! = {world = "World" Stdout.line!("Hello, world!")}
,
\\app [main!] { pf: platform "../basic-cli/platform.roc" }
\\
\\import pf.Stdout
\\
\\main! = {
\\ world = "World"
\\ Stdout.line!("Hello, world!")
\\}
);
}
❯ zig build test --summary all
Build Summary: 5/5 steps succeeded; 9/9 tests passed
test success
└─ run test 9 passed 256ms MaxRSS:2M
└─ zig test Debug native success 1s MaxRSS:368M
└─ run gencat (gencat.bin.z) cached
└─ zig build-exe gencat Debug native cached 27ms MaxRSS:32M
wow, that was fast! :heart_eyes:
It ended up being pretty easy
One question for you @Richard Feldman because I haven't seen it in the thread (I also have like 1000 unread Zulip messages right now). Are we allowing arbitrary block expressions?
I ask because it makes parsing records slightly more annoying, but I totally understand we may want it
To be clear I mean
foo = {
some_fn!()?
some_other_fn!()?
some_expr
}
or
foo = {
bar: {
some_fn!()?
some_other_fn!()?
some_expr
},
}
oh yeah for sure, aside from the semicolon :big_smile:
Sorry, zig brain. Fixed
Hmm, I am a bit concerned that this means we have to look ahead an unbounded number of tokens to determine if we should be parsing a type decl or a record field
And that is even pretty confusing to the eye
hm, pretty sure it's just 1 token unless I'm missing something?
{ followed by lowercase identifier followed by either , or : is a record
anything else is a block
yes at least three non-whitespace tokens
That's a fair amount of lookahead - especially when you add in potential newlines
I think I'm only slightly worried due to the importance of records in the language
I know what to do here, just calling it out. There's similar problems with tuples and parenthesized expressions (luckily tuples are typically pretty rare)
Tuples and parenthesized exprs and less bad because they’re both exprs
Here is a record and a block, where we won't be able to determine whether we should be parsing a type or an expr until we see the thing after qux::
foo = {
bar: {
baz: {
qux: 42,
}
}
}
foo = {
bar: {
baz: {
qux: Str -> Str,
}
}
bar = {
baz: {
qux: |s| s,
}
}
bar
}
I can construct other such examples that further delay that distinction arbitrarily.
Just reading that, it's hard to tell what's going on, which is IMO a readability problem
ah good point! I forgot about the inline type annotation implication
well the reason it's confusing to read is that it's bar: whereas type annotations are always written as bar :
That's a _very_ subtle distinction to expect a new user to pick up on
we could use that space (or lack of a space) to guess which path to go down
I don't expect new users to encounter anything that looks like this :big_smile:
almost all inline annotations are 1 line
we could use that space (or lack of a space) to guess which path to go down
That doesn't simplify the parser, unless we're allowed to give an error and bail out if we don't see that.
true, unless we want to backtrack :sweat_smile:
No, no, no
No backtracking
Allowing/needing backtracking is IMO a sign of a poorly-designed language grammar
It is indicative of issues that affect not just whether the machine can parse the language, but how easy it is for humans to parse as well
And furthermore, it's easy for it to become a performance blackhole that can make fuzzing difficult
yeah I don't actually want to backtrack :laughing:
but yeah, overall this reminds me of type inference on local declarations in general: technically Hindley-Milner type inference has bad asymptotics on them, so if you made a gigantic number of local variables in a row it would really hurt performance, but in practice nobody notices because people don't write real-world code that way
like pretty much all annotated inline defs will look like this:
foo :
foo =
and I don't think those will be confusing to read
Braces seem to be the consensus solution for implementing #ideas > insignificant whitespace ! Any objection to me resolving that centithread kilothread?
It sounds like the path forward here is to create a NoSpaceColon token that we output if there's no whitespace before the colon. Use that one for records, and the normal Colon for types. Give an error if you use the wrong one in a context that's unambiguous, and if it's otherwise ambiguous, use it for disambiguation.
(side note: I'm still skeptical of the readability here, but that's a larger discussion that we definitely need more evidence for...)
Well, really a body is an expression
Yes, but we can't tell whether the rhs of that first ':' is a type or a record expr, without this rule about NoSpaceColon vs Colon.
hm, will that rule be a problem if people write a record type as { name: Str } without the space, because that's what they're used to from other languages?
If they do so in a context where, up-to-that-point, it's ambiguous whether that's a type or a record literal, they'll get an error (so, yes)
That error could be designed to guide them to fix the problem
ah I'd mostly expect that to come up in a top-level type alias
or in a function type annotation
so prob fine then!
I wonder how single field records of reasonml work with the if/else expressions
also, have we considered partial applications?
fn = |x| {
|y| { x + y }
}
fn = |x| |y| { x + y }
Seems like it'd work just fine!
Yeah, that is the same as \x -> \y -> ... today. Should just work.
Sorry for the basic question, but why are braces needed? Why is it insufficient to parse a function as a series of assignments ending in an expression? Is it because of calling an effectual function that doesn't assign to anything? Are there other ambiguous cases?
Braces are being used for multiline functions to remove white space significance from the language. This enables the parser to be much more tolerant of various code formats and makes copy and paste just work. Those are a least the top two things that come to mind for me.
a way to think of braces is that they're a way to add statements to an expression
so if I have an expression like foo I can add statements in front of it using braces, e.g.
{
x = bar * 2
expect bar == baz
foo + x
}
so that whole braces-enclosed thing is an expression
Oh, tangential question
Do we plan to support stand alone braces for scoping. Super simple example
fn! = || {
{
x = read!()
write!(x)
}
{
x = read!()
write!(x)
}
}
yep!
I was writing up docs for them and I think :point_up: is the actual definition we want
like "if you want to add statements in front of an expression, surround both the statements and the expression in { ... }. That whole { ... } is called a block, and it is an expression."
then, separately, there's the rule that "aside from the expression at the very end of a block, anything inside that block which looks like a standalone expression rather than a statement desugars to having {} = in front of it" - e.g. a write!(x) in the middle of a bunch of statements desugars into {} = write!(x)
and that's it, that's the complete explanation of how { ... } blocks work
(of course { .... } also comes up in records, as well as delimiting the list of patterns in a match, as well as in some module headers)
Brendan Hansknecht said:
Oh, tangential question
Do we plan to support stand alone braces for scoping. Super simple example
fn! = || { { x = read!() write!(x) } { x = read!() write!(x) } }
This is working in my latest PR. A block is just an expression that contains a series of atatements
So you can pass them as function args, have them as list items even have them as the predicate of an if
Function args?
Haha
That sounds like terrible syntax, but I guess it makes sense it can work anywhere
Yeah don’t do it, but you could
I guess I could see someone doing something like this (just with a different context).
out = my_list.map({
calculation_to_cache = something_super_slow!(...)
|x| update(x, calculation_to_cache)
})
yeah I think the main benefit is just the conceptual simplicity of the rule
so it's easier to understand how it works and what you can do with it
Yep
These brackets as Brendan Hansknecht defined look an awful lot like the way fusion of the let-in and () from Elm.
fn =
fn1
( let a = b + 123
in fn2 a b
)
c
Which in Roc becomes
fn =
fn1
( a = b + 123
fn2 a b
)
c
Wouldn't it make sense to simply extend the definition of those parenthesis instead of using the brackets?
We talked about that, but it ends up being semantic overload on ()....it would be apply args, tuples, parenthesized expressions, AND blocks.
The last two have a lot of contention
Yup. Since "whitespace application" ("WSA") is deprecated, it would actually be
fn =
fn1(
(
a = b + 123
fn2(a, b)
),
c
)
which reads better as
fn =
fn1(
{
a = b + 123
fn2(a, b)
},
c
)
I just realized. the following is possible as well, right? kinda funny but why not :D
x = 42 - { 21 * 2 }
on the other hand... block expressions feel like a multi-statement version of parens. does it make sense to require more than a single statement in the block? or maybe fmt may help?
yeah, I know, noone sane would write this kind of code anyway
Maybe the formatter should convert braces to parens if they only contain a single expression.
(and then it can do the existing pass to remove excess parens)
Yes a block with a single expression and no newlines/comments will have the braces removed by the formatter
unless it's if a { b } else { c }, right? :big_smile:
it feels like body (either of if/else or function) is not the same as block expr on the ir level. but both can be records ofc
I think it's just a formatting thing
for if only
If there were no records syntax, you could do this:
# function that returns one
returns_one = { 1 }
# traditional function
identity = { x -> x }
add = { a: I32, b: I32 -> a + b }
# function with multiple "cases", replaces when-is / match:
is_ok = {
Ok _ -> Bool.true,
Err _ -> Bool.false
}
divide = { a, b ->
b |> {
0 -> crash "can't.",
_ -> a / b
}
}
whether this is good/readable, i don't know.
I haven't read the whole discussion here so I might be repeating someone but is the aim here only to make white spaces insignificant? If that's the only goal I personally think that enforcing readability is more important.
In JS, you can stuff any amount of code on a single line and make it completely unreadable thanks to the fact that spaces are insignificant in that language. That also gives potential for malicious code to be hiding in.
For JS that makes sense because you want your code to be as tiny as possible and remove every possible character when sending it to client's browsers. But I think that doesn't work for Roc as it is compiled.
Completely leaning on indentation and new lines for the language syntax might also be a viable approach. It would force both users and code generators to make code with a structure that can be understood at first glance. I think that actually would be a feature.
I agree that readability is more important than write-ability, but I think that this isn't a drop in readability. I would prefer the aesthetic of whitespaces over braces, but I think braces are actually very easy to read
And since it doesn't seem like a drop in readability, a less frustrating experience when copy-pasting code and working with LLMs and writing unindented code (which braces are better at for all three of these experiences) seems like a good trade-off
If it was a drop in readability, then we'd want to maybe reconsider more strongly
Note: we also have an opinionated formatter that will stop the super giant single line of code thing.
Joshua Warner said:
if foo { 1 } else {2}
I've been thinking about if-then-else ... I keep getting tripped up on not having then and I see other people making the same mistake.
I understand that we decided to try not having it because then that is one less keyword, and then is free for people to use as a variable name, and it isn't needed with the braces design.
But I'm wondering if the cost from a strangeness budget is larger than we thought when we considered this design.
I also think since we had that discussion we also decided that the braces are optional because they're for block expressions.
Basically after writing a fair amount of 0.1 roc (in the snapshots) I'm definitely feeling like we should re-consider then.
i agree with this. I never struggle with this in other languages that don't require parens around the condition because they require {} around the expressions
i think then is helpful, or some other change is needed
Like require then if the next expression isn't a block
This is why python changes the order, right?
1 if foo else 2
probably
it seems the human needs a keyword or punctuation even if the machine doesn't :rolling_on_the_floor_laughing:
Luke Boswell said:
Joshua Warner said:
if foo { 1 } else {2}I've been thinking about if-then-else ... I keep getting tripped up on not having
thenand I see other people making the same mistake.
is that because you're used to the old Roc syntax though?
the only mainstream programming languages that have then are Bash and Ruby, so I don't think "lack of then" can possibly be the problem here
if lack of curly braces is the problem, we can just have a convention of putting curly braces around the branches, right?
i don't know why it is, but i keep putting parens around the condition or braces around the expression or I as a person have trouble reading it
sure, but both of those Just Work, right?
yep
the formatter currently doesn't have that style
right, but to me this seems like a formatter question and not a syntax design question
like I don't see a case for reintroducing then or switching to 1 if foo or anything like that
so maybe the solve is just to have it output braces
That's probably right
yeah the lowest strangeness budget way to address these concerns is to have the formatter use one or more of these interventions that are already supported
As long as the thing most people will do just works, then it's all about the conventional style
And formatter
it's nice that people can write a C style if statement and it'll just work
And we probably will turn it at worst to a Go-style if statement
in the formatter
assuming we unwrap useless parenthesized expressions
I think it does feel odd due to feeling like a record
I think doing it the way Go and Rust do, where we have braces around the branches and no parens around the conditionals, should be uncontroversial
Also, does any language have this syntax?
But we should keep {}s i think
Brendan Hansknecht said:
Also, does any language have this syntax?
with the curly braces it is 100% exactly how Rust does it
including the branches being expressions
Ah yeah. They just always require braces and we don't?
e.g. this is valid Rust code that does the same thing as what it would do in Roc:
if foo {
a
} else {
b
}
right
Ok
so if the formatter adds braces, then it's just Rust code
honestly my only hesitation for requiring that the formatter add braces is that it creates an inconsistency
without special-casing if, the rule can be consistently:
if foo { 1 } else { 2 }
uses braces but is single-line, so the simple formatter rule of "braces == multiline" would turn this into multiline
Richard Feldman said:
is that because you're used to the old Roc syntax though?
This is probably a major factor then. For some reason I thought if-then-else was the norm in most languages... but I haven't really touched anything besides Rust/Zig or Roc for a while now. I'm definitely a minority here, I just wanted to flag it because I've been tripping up on it.
Ironically Rust and Zig don't have then... so I'm definitely getting this from some place else
It must be current Roc's influence... or maybe even I'm having a stroke and think I'm writing VB
I think as usual it comes down to what you’re used to. See my other thread where - well unhelpful error message aside - I first tripped on the bracketing, then on needing then. Simply due to me being used to if (condition) {code block when true}. I personally would opt for the curly ones even when single line (also in js) just to have consistency. Also when copying something over where the formatting is partially lost that gives another visual cue.
Personally I really don’t grasp the conciseness factor in a lot of things (take e.g. something like arr for array or err for error) given modern tooling can autocomplete easily. Having worked more on the enterprise side of things with larger teams but also quite a lot of business users all of these are possible places for different understanding or confusion :sweat_smile:
Tldr: Would prefer a uniform approach, not here’s single line expression variant, here’s the multiline one perhaps heres single condition or the like
Last updated: Jun 16 2026 at 16:19 UTC