Stream: ideas

Topic: braces syntax


view this post on Zulip Richard Feldman (Feb 11 2025 at 20:47):

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

view this post on Zulip Richard Feldman (Feb 11 2025 at 20:47):

here's a diff on the roc-realworld code base

view this post on Zulip Richard Feldman (Feb 11 2025 at 20:48):

I recommend looking at individual files too, such as main.roc or Article.roc

view this post on Zulip Richard Feldman (Feb 11 2025 at 20:51):

the way this syntax works compared to today is:

  1. Lambda expressions have exactly 1 expression for their body
  2. We introduce the concept of { ... } 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 { ... }.
  3. As it turns out, a lot of lambdas (in this code base at least) don't need the braces because they're just one expression anyway. So those all look exactly the same as today.
  4. 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.)

view this post on Zulip Richard Feldman (Feb 11 2025 at 20:53):

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)

view this post on Zulip Richard Feldman (Feb 11 2025 at 20:53):

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.

view this post on Zulip Richard Feldman (Feb 11 2025 at 20:54):

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.

view this post on Zulip Richard Feldman (Feb 11 2025 at 20:55):

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!

view this post on Zulip Sam Mohr (Feb 11 2025 at 20:57):

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)
    },
}

view this post on Zulip Joshua Warner (Feb 11 2025 at 20:59):

Coming from using 90+% {}-delimited languages thru-out my career (C/C++, Java, Javascript, Typescript, Rust), this definitely makes me feel more comfortable

view this post on Zulip Sam Mohr (Feb 11 2025 at 20:59):

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

view this post on Zulip Joshua Warner (Feb 11 2025 at 20:59):

It also makes parsing a whole lot easier

view this post on Zulip Sam Mohr (Feb 11 2025 at 20:59):

It's non-intrusive enough that I'd be okay with this

view this post on Zulip Joshua Warner (Feb 11 2025 at 20:59):

I think my primary concern here would be alienating other members of the community

view this post on Zulip Richard Feldman (Feb 11 2025 at 21:01):

I'm definitely curious to hear what community members think here! :smiley:

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 21:03):

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.

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 21:04):

I personally am 100% in on auto formatting that everyone should use. I think braces make it easier for that to be consistent.

view this post on Zulip Luke Boswell (Feb 11 2025 at 21:18):

I like this syntax.

view this post on Zulip Luke Boswell (Feb 11 2025 at 21:19):

The lambda expressions have just one expression for thier body seems new... but it feels familiar in practice.

view this post on Zulip Dawid Danieluk (Feb 11 2025 at 21:27):

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

view this post on Zulip Richard Feldman (Feb 11 2025 at 21:31):

I think "good, and familiar" is good, and "bad, but familiar" is bad

view this post on Zulip Richard Feldman (Feb 11 2025 at 21:31):

so as long as it seems good, familiar on top of that is even better!

view this post on Zulip Jared Ramirez (Feb 11 2025 at 21:38):

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.

view this post on Zulip Jared Ramirez (Feb 11 2025 at 21:38):

That said, I like the syntax!

view this post on Zulip Dawid Danieluk (Feb 11 2025 at 21:38):

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" :-)

view this post on Zulip Joshua Warner (Feb 11 2025 at 21:43):

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?

view this post on Zulip Joshua Warner (Feb 11 2025 at 21:48):

(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)

view this post on Zulip Joshua Warner (Feb 11 2025 at 21:50):

(I guess this also applies to the do/end proposal as well)

view this post on Zulip Richard Feldman (Feb 11 2025 at 22:02):

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

view this post on Zulip Richard Feldman (Feb 11 2025 at 22:03):

indentation is what that causes problems

view this post on Zulip Richard Feldman (Feb 11 2025 at 22:05):

concretely, so I'd say:

  x!()-x!()

equivalent to x!() - x!()

  x!() -x!()

equivalent to:

  x!()
  -x!()
  x!()
    -x!()

also equivalent to the above

view this post on Zulip Richard Feldman (Feb 11 2025 at 22:05):

so only the first one would be different

view this post on Zulip Richard Feldman (Feb 11 2025 at 22:11):

- has always been full of edge cases :stuck_out_tongue:

view this post on Zulip Sam Mohr (Feb 11 2025 at 22:21):

So how would I write something like this?

debugged_negative_epoch = ||
    Stdout.line!("Getting the negative epoch")
    -get_epoch!()
}

view this post on Zulip Sam Mohr (Feb 11 2025 at 22:22):

Wouldn't that be interpreted as line! - get_epoch!

view this post on Zulip Richard Feldman (Feb 11 2025 at 22:35):

nah that's what I mean about being newline-sensitive being fine

view this post on Zulip Richard Feldman (Feb 11 2025 at 22:36):

also in that case it's prob enough to just see that the dash is touching on one side only

view this post on Zulip Anthony Bullard (Feb 11 2025 at 22:37):

Unary ops have to touch

view this post on Zulip Anthony Bullard (Feb 11 2025 at 22:38):

Same with trailing ?

view this post on Zulip Anthony Bullard (Feb 11 2025 at 22:38):

It has to be attached to the expression

view this post on Zulip Luke Boswell (Feb 11 2025 at 22:38):

Well for ? a space before means something different

view this post on Zulip Anthony Bullard (Feb 11 2025 at 22:40):

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.

view this post on Zulip Joshua Warner (Feb 11 2025 at 22:45):

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?

view this post on Zulip Joshua Warner (Feb 11 2025 at 22:46):

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.

view this post on Zulip Joshua Warner (Feb 11 2025 at 22:49):

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.

view this post on Zulip Anthony Bullard (Feb 11 2025 at 22:52):

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

view this post on Zulip Joshua Warner (Feb 11 2025 at 22:52):

huh

view this post on Zulip Joshua Warner (Feb 11 2025 at 22:54):

: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.

view this post on Zulip Joshua Warner (Feb 11 2025 at 22:54):

But yeah, fair, I certainly can't refute that

view this post on Zulip Anthony Bullard (Feb 11 2025 at 22:54):

It's possible!

view this post on Zulip Anthony Bullard (Feb 11 2025 at 22:55):

Wouldn't you immediately in the case above get a type error "Can't subtract {}"

view this post on Zulip Anthony Bullard (Feb 11 2025 at 22:55):

And then you would get a problem and move on...?

view this post on Zulip Richard Feldman (Feb 11 2025 at 22:56):

yeah I think it's 100% fine to assume people only want unary op if it's touching

view this post on Zulip Joshua Warner (Feb 11 2025 at 22:56):

I think we probably ought to have the formatter indent that line to make things clear tho

view this post on Zulip Richard Feldman (Feb 11 2025 at 22:57):

totally! :+1:

view this post on Zulip Anthony Bullard (Feb 11 2025 at 22:57):

I agree with that!

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:00):

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

view this post on Zulip Richard Feldman (Feb 11 2025 at 23:01):

I actually like the idea of having the parser detect semicolons and then we have the formatter discard them for you

view this post on Zulip Richard Feldman (Feb 11 2025 at 23:01):

then we could give a warning like "Roc doesn't have semicolons. You're free!"

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:01):

We could do what Go does and treat them as whitespace

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:01):

Or, at least end-of-line whitespace

view this post on Zulip Richard Feldman (Feb 11 2025 at 23:02):

sure, and then have the formatter replace them with an actual newline

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:02):

Yep, have it (collapsed with a following newline if it exists) into a newline token

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:02):

That should reliably get what you want in the formatter

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:03):

that's trivial actually

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:04):

Man, semicolons, braces, and PNC. What is this C/Algol descendant? :rofl:

view this post on Zulip Richard Feldman (Feb 11 2025 at 23:12):

I think this should help out tree-sitter a lot :big_smile:

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:13):

Definitely

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:13):

Also, we can now have a context-free grammar

view this post on Zulip Richard Feldman (Feb 11 2025 at 23:13):

hm, is that true?

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:13):

Which will make all manner of tooling easier

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:13):

I think so...

view this post on Zulip Richard Feldman (Feb 11 2025 at 23:13):

(maybe it is! I hadn't actually thought about it)

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:14):

WSS is what makes context-free impossible typically

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:14):

because the indent level is context you have to track

view this post on Zulip Richard Feldman (Feb 11 2025 at 23:14):

right, but I guess the question is - are there other remaining blockers?

view this post on Zulip Richard Feldman (Feb 11 2025 at 23:14):

like maybe something related to string interpolation (but probably not that one)

view this post on Zulip Richard Feldman (Feb 11 2025 at 23:14):

yeah maybe it is!

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:14):

I'm no expert here, maybe @Joshua Warner can chime in. But I don't think so

view this post on Zulip Joshua Warner (Feb 11 2025 at 23:17):

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

view this post on Zulip Richard Feldman (Feb 11 2025 at 23:18):

what if they were just "line begins with """ and ends with newline, and indentation doesn't matter"?

view this post on Zulip Richard Feldman (Feb 11 2025 at 23:18):

and then as many consecutive lines of those as you have, they all go together in one string literal

view this post on Zulip Joshua Warner (Feb 11 2025 at 23:19):

I think that does it, yeah

view this post on Zulip Richard Feldman (Feb 11 2025 at 23:19):

so theoretically you could do like:

my_str =
    """foo
  """bar
       """baz

and then the formatter takes care of lining them up for you

view this post on Zulip Richard Feldman (Feb 11 2025 at 23:19):

that seems like the error-tolerant way to do that design anyway :big_smile:

view this post on Zulip Anthony Bullard (Feb 11 2025 at 23:19):

I like it

view this post on Zulip Notification Bot (Feb 11 2025 at 23:37):

12 messages were moved from this topic to #ideas > multiline string syntax by Luke Boswell.

view this post on Zulip Elias Mulhall (Feb 12 2025 at 01:43):

Does braces mean roc is entering its awkward tween years?

view this post on Zulip Richard Feldman (Feb 12 2025 at 01:43):

it is almost zero point oneteen years old!

view this post on Zulip Richard Feldman (Feb 12 2025 at 01:43):

("almost" doing a lot of work there)

view this post on Zulip Isaac Van Doren (Feb 12 2025 at 03:07):

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:

view this post on Zulip Sam Mohr (Feb 12 2025 at 03:19):

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

view this post on Zulip Sam Mohr (Feb 12 2025 at 03:19):

This probably would only make sense for lambdas

view this post on Zulip Sam Mohr (Feb 12 2025 at 03:19):

Since if and else would look weird if we did it to them

view this post on Zulip Isaac Van Doren (Feb 12 2025 at 03:20):

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

view this post on Zulip Sam Mohr (Feb 12 2025 at 03:21):

True

view this post on Zulip Sam Mohr (Feb 12 2025 at 03:22):

If you control when the formatter runs, it works, but if you have it run on auto save it'll get annoying

view this post on Zulip Richard Feldman (Feb 12 2025 at 03:26):

yeah I think if and else and match should all require the braces

view this post on Zulip Sam Mohr (Feb 12 2025 at 03:26):

Don't we want to support one-line if and else?

view this post on Zulip Sam Mohr (Feb 12 2025 at 03:26):

I understand if and else wanting to both have braces if one of them does

view this post on Zulip Joshua Warner (Feb 12 2025 at 03:27):

if foo { 1 } else {2}

view this post on Zulip Joshua Warner (Feb 12 2025 at 03:27):

That said, I wouldn't be sad if we stripped away braces for expressions below some complexity level

view this post on Zulip Joshua Warner (Feb 12 2025 at 03:28):

e.g. constants and variables are fine to have outside of braces. Everything else probably deserves braces. (arguable!)

view this post on Zulip Richard Feldman (Feb 12 2025 at 03:28):

eh I don't think the formatter needs to mess with removing or adding braces

view this post on Zulip Joshua Warner (Feb 12 2025 at 03:29):

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.

view this post on Zulip Richard Feldman (Feb 12 2025 at 03:29):

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

view this post on Zulip Sam Mohr (Feb 12 2025 at 03:31):

Yeah...

view this post on Zulip Sam Mohr (Feb 12 2025 at 03:31):

Responding to the if else brace example before Zulip loaded the messages

view this post on Zulip Ray Myers (Feb 12 2025 at 06:35):

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

view this post on Zulip Norbert Hajagos (Feb 12 2025 at 08:09):

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.

view this post on Zulip Norbert Hajagos (Feb 12 2025 at 08:34):

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.

view this post on Zulip Sky Rose (Feb 12 2025 at 14:52):

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?

view this post on Zulip Niclas Ahden (Feb 12 2025 at 15:06):

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.

view this post on Zulip Niclas Ahden (Feb 12 2025 at 15:07):

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.

view this post on Zulip Niclas Ahden (Feb 12 2025 at 15:09):

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.

view this post on Zulip Niclas Ahden (Feb 12 2025 at 15:17):

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:

view this post on Zulip Richard Feldman (Feb 12 2025 at 15:30):

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

view this post on Zulip Richard Feldman (Feb 12 2025 at 15:32):

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

view this post on Zulip Richard Feldman (Feb 12 2025 at 15:32):

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

view this post on Zulip Richard Feldman (Feb 12 2025 at 15:33):

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:

view this post on Zulip Richard Feldman (Feb 12 2025 at 15:36):

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:

view this post on Zulip Richard Feldman (Feb 12 2025 at 15:36):

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:

view this post on Zulip jan kili (Feb 12 2025 at 20:53):

I'm surprised

view this post on Zulip Elias Mulhall (Feb 13 2025 at 05:40):

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

view this post on Zulip Brendan Hansknecht (Feb 13 2025 at 06:01):

I like reason's resolution

view this post on Zulip Agus Zubiaga (Feb 14 2025 at 02:52):

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.

view this post on Zulip Richard Feldman (Feb 15 2025 at 13:56):

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 }

view this post on Zulip Anthony Bullard (Feb 15 2025 at 16:30):

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 c and the formatter changes it if a { b } else { c }

I think we'll have to talk about this.

view this post on Zulip Anthony Bullard (Feb 15 2025 at 16:30):

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.

view this post on Zulip Anthony Bullard (Feb 15 2025 at 16:31):

Where a block replaces the concept of Defs exprs

view this post on Zulip Joshua Warner (Feb 15 2025 at 16:32):

New lines are still important tho

view this post on Zulip Anthony Bullard (Feb 15 2025 at 16:32):

So any def in a lambda body, or any "statement" like a null def, has to be in a block

view this post on Zulip Anthony Bullard (Feb 15 2025 at 16:32):

Of course

view this post on Zulip Anthony Bullard (Feb 15 2025 at 16:32):

Every statement in a block must be followed by a newline

view this post on Zulip Anthony Bullard (Feb 15 2025 at 16:34):

So inside of a block there are three broad classes of statements:

  1. Defs - assigning the value of a single expression OR block to a pattern - must not be in block-terminal position
  2. Null defs - a single expression not in the terminal position where the value is ignored
  3. Exprs - A block-terminal expression that represents the value of the block

view this post on Zulip Anthony Bullard (Feb 15 2025 at 16:35):

The name for #2 above is up for debate

view this post on Zulip Joshua Warner (Feb 15 2025 at 16:36):

For the purpose of parsing and formatting 2 and 3 are the same thing I think

view this post on Zulip Anthony Bullard (Feb 15 2025 at 16:36):

Yes, actually #2 and #3 no longer have to be separate

view this post on Zulip Anthony Bullard (Feb 15 2025 at 16:36):

We will only have to report a problem if a block ends in a def

view this post on Zulip Richard Feldman (Feb 15 2025 at 16:36):

hm, did it end up being true that newlines are different from spaces?

view this post on Zulip Richard Feldman (Feb 15 2025 at 16:37):

I'm ok if so, I just thought the parser could treat them as equivalent with no problem

view this post on Zulip Anthony Bullard (Feb 15 2025 at 16:38):

You mean that:

|a| { b = 1 c = 2 d = 4 a + b + c + d }

Would be accepted?

view this post on Zulip Richard Feldman (Feb 15 2025 at 16:38):

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

view this post on Zulip Richard Feldman (Feb 15 2025 at 16:38):

yeah exactly

view this post on Zulip Richard Feldman (Feb 15 2025 at 16:38):

like obviously the formatter should rewrite that to have newlines

view this post on Zulip Richard Feldman (Feb 15 2025 at 16:38):

but I think it's unambiguous for the parser to accept it

view this post on Zulip Anthony Bullard (Feb 15 2025 at 16:38):

Trying to think of the places where that is difficult....

view this post on Zulip Joshua Warner (Feb 15 2025 at 16:38):

There are some key places where we absolutely have to still pay attention to new lines (unless we make further syntax changes)

view this post on Zulip Anthony Bullard (Feb 15 2025 at 16:38):

You are probably right that we could

view this post on Zulip Joshua Warner (Feb 15 2025 at 16:39):

I had a post about this, one sec

view this post on Zulip Richard Feldman (Feb 15 2025 at 16:39):

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?

view this post on Zulip Joshua Warner (Feb 15 2025 at 16:40):

https://roc.zulipchat.com/#narrow/stream/395097-compiler-development/topic/Indent.20insensitive.20parsing.20and.20disallowing.20some.20line.20breaks

view this post on Zulip Joshua Warner (Feb 15 2025 at 16:40):

Should have posted that in this channel, come to think of it

view this post on Zulip Richard Feldman (Feb 15 2025 at 16:41):

I wish those links worked in Zulip's mobile app :sweat_smile:

view this post on Zulip Joshua Warner (Feb 15 2025 at 16:41):

Oof

view this post on Zulip Richard Feldman (Feb 15 2025 at 16:41):

can you quote it?

view this post on Zulip Joshua Warner (Feb 15 2025 at 16:42):

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)

view this post on Zulip Richard Feldman (Feb 15 2025 at 16:51):

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:

view this post on Zulip Joshua Warner (Feb 15 2025 at 17:02):

Ahhh interesting

view this post on Zulip Joshua Warner (Feb 15 2025 at 17:03):

I still think we should warn if you don’t have a new line between statements in a block

view this post on Zulip Richard Feldman (Feb 15 2025 at 17:05):

yeah I think warning is fine :thumbs_up:

view this post on Zulip Richard Feldman (Feb 15 2025 at 17:06):

but I do like the idea of all whitespace being interchangeable if we can get away with it

view this post on Zulip Richard Feldman (Feb 15 2025 at 17:07):

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

view this post on Zulip Richard Feldman (Feb 15 2025 at 17:10):

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

view this post on Zulip Richard Feldman (Feb 15 2025 at 17:11):

I did just realize that needs braces

view this post on Zulip Richard Feldman (Feb 15 2025 at 17:11):

because you only get to have whitespace-separated expressions and statements inside braces

view this post on Zulip Joshua Warner (Feb 15 2025 at 17:13):

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.

view this post on Zulip Richard Feldman (Feb 15 2025 at 17:13):

this is also why I like allowing (but warning and reformatting) if a b else c

view this post on Zulip Richard Feldman (Feb 15 2025 at 17:14):

because it can be taught that blocks ({ ... }) can be used anywhere an expression is accepted

view this post on Zulip Richard Feldman (Feb 15 2025 at 17:15):

and then we still have the property that else if is not special - it's just if a b else if c else d

view this post on Zulip Richard Feldman (Feb 15 2025 at 17:15):

and we choose to require braces as a matter of formatting style to make things read better

view this post on Zulip jan kili (Feb 15 2025 at 17:18):

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

view this post on Zulip Richard Feldman (Feb 15 2025 at 17:24):

I can't think of any reasons we'd want to format it like that :sweat_smile:

view this post on Zulip Joshua Warner (Feb 15 2025 at 17:38):

If you have short variables or literals, if foo 1 else 2 has less visual noise than if foo { 1 } else { 2 }

view this post on Zulip Joshua Warner (Feb 15 2025 at 17:39):

It does look kinda weird tho...

view this post on Zulip jan kili (Feb 15 2025 at 19:19):

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.

view this post on Zulip Anthony Bullard (Feb 16 2025 at 12:10):

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

view this post on Zulip Richard Feldman (Feb 16 2025 at 12:43):

wow, that was fast! :heart_eyes:

view this post on Zulip Anthony Bullard (Feb 18 2025 at 15:09):

It ended up being pretty easy

view this post on Zulip Anthony Bullard (Feb 18 2025 at 15:10):

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?

view this post on Zulip Anthony Bullard (Feb 18 2025 at 15:10):

I ask because it makes parsing records slightly more annoying, but I totally understand we may want it

view this post on Zulip Anthony Bullard (Feb 18 2025 at 15:12):

To be clear I mean

foo = {
    some_fn!()?
    some_other_fn!()?
    some_expr
}

or

foo = {
    bar: {
        some_fn!()?
        some_other_fn!()?
        some_expr
    },
}

view this post on Zulip Richard Feldman (Feb 18 2025 at 15:42):

oh yeah for sure, aside from the semicolon :big_smile:

view this post on Zulip Anthony Bullard (Feb 18 2025 at 15:50):

Sorry, zig brain. Fixed

view this post on Zulip Joshua Warner (Feb 18 2025 at 17:31):

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

view this post on Zulip Joshua Warner (Feb 18 2025 at 17:32):

And that is even pretty confusing to the eye

view this post on Zulip Richard Feldman (Feb 18 2025 at 17:36):

hm, pretty sure it's just 1 token unless I'm missing something?

view this post on Zulip Richard Feldman (Feb 18 2025 at 17:36):

{ followed by lowercase identifier followed by either , or : is a record

view this post on Zulip Richard Feldman (Feb 18 2025 at 17:37):

anything else is a block

view this post on Zulip Anthony Bullard (Feb 18 2025 at 17:40):

yes at least three non-whitespace tokens

view this post on Zulip Anthony Bullard (Feb 18 2025 at 17:41):

That's a fair amount of lookahead - especially when you add in potential newlines

view this post on Zulip Anthony Bullard (Feb 18 2025 at 17:41):

I think I'm only slightly worried due to the importance of records in the language

view this post on Zulip Anthony Bullard (Feb 18 2025 at 17:43):

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)

view this post on Zulip Joshua Warner (Feb 18 2025 at 17:48):

Tuples and parenthesized exprs and less bad because they’re both exprs

view this post on Zulip Joshua Warner (Feb 18 2025 at 17:56):

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.

view this post on Zulip Joshua Warner (Feb 18 2025 at 17:57):

Just reading that, it's hard to tell what's going on, which is IMO a readability problem

view this post on Zulip Richard Feldman (Feb 18 2025 at 17:57):

ah good point! I forgot about the inline type annotation implication

view this post on Zulip Richard Feldman (Feb 18 2025 at 17:58):

well the reason it's confusing to read is that it's bar: whereas type annotations are always written as bar :

view this post on Zulip Joshua Warner (Feb 18 2025 at 17:59):

That's a _very_ subtle distinction to expect a new user to pick up on

view this post on Zulip Richard Feldman (Feb 18 2025 at 17:59):

we could use that space (or lack of a space) to guess which path to go down

view this post on Zulip Richard Feldman (Feb 18 2025 at 17:59):

I don't expect new users to encounter anything that looks like this :big_smile:

view this post on Zulip Richard Feldman (Feb 18 2025 at 18:00):

almost all inline annotations are 1 line

view this post on Zulip Joshua Warner (Feb 18 2025 at 18:00):

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.

view this post on Zulip Richard Feldman (Feb 18 2025 at 18:00):

true, unless we want to backtrack :sweat_smile:

view this post on Zulip Joshua Warner (Feb 18 2025 at 18:00):

No, no, no

view this post on Zulip Joshua Warner (Feb 18 2025 at 18:00):

No backtracking

view this post on Zulip Joshua Warner (Feb 18 2025 at 18:19):

Allowing/needing backtracking is IMO a sign of a poorly-designed language grammar

view this post on Zulip Joshua Warner (Feb 18 2025 at 18:20):

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

view this post on Zulip Joshua Warner (Feb 18 2025 at 18:20):

And furthermore, it's easy for it to become a performance blackhole that can make fuzzing difficult

view this post on Zulip Richard Feldman (Feb 18 2025 at 18:23):

yeah I don't actually want to backtrack :laughing:

view this post on Zulip Richard Feldman (Feb 18 2025 at 19:07):

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

view this post on Zulip Richard Feldman (Feb 18 2025 at 19:08):

like pretty much all annotated inline defs will look like this:

foo :
foo =

view this post on Zulip Richard Feldman (Feb 18 2025 at 19:09):

and I don't think those will be confusing to read

view this post on Zulip jan kili (Feb 18 2025 at 19:15):

Braces seem to be the consensus solution for implementing #ideas > insignificant whitespace ! Any objection to me resolving that centithread kilothread?

view this post on Zulip Joshua Warner (Feb 18 2025 at 19:33):

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.

view this post on Zulip Joshua Warner (Feb 18 2025 at 19:34):

(side note: I'm still skeptical of the readability here, but that's a larger discussion that we definitely need more evidence for...)

view this post on Zulip Anthony Bullard (Feb 18 2025 at 22:36):

Well, really a body is an expression

view this post on Zulip Joshua Warner (Feb 18 2025 at 23:14):

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.

view this post on Zulip Richard Feldman (Feb 18 2025 at 23:43):

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?

view this post on Zulip Joshua Warner (Feb 18 2025 at 23:44):

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)

view this post on Zulip Joshua Warner (Feb 18 2025 at 23:45):

That error could be designed to guide them to fix the problem

view this post on Zulip Richard Feldman (Feb 18 2025 at 23:45):

ah I'd mostly expect that to come up in a top-level type alias

view this post on Zulip Richard Feldman (Feb 18 2025 at 23:45):

or in a function type annotation

view this post on Zulip Richard Feldman (Feb 18 2025 at 23:45):

so prob fine then!

view this post on Zulip Kiryl Dziamura (Feb 19 2025 at 12:28):

I wonder how single field records of reasonml work with the if/else expressions

view this post on Zulip Kiryl Dziamura (Feb 20 2025 at 09:26):

also, have we considered partial applications?

fn = |x| {
    |y| { x + y }
}

fn = |x| |y| { x + y }

view this post on Zulip Sam Mohr (Feb 20 2025 at 09:44):

Seems like it'd work just fine!

view this post on Zulip Brendan Hansknecht (Feb 25 2025 at 17:39):

Yeah, that is the same as \x -> \y -> ... today. Should just work.

view this post on Zulip Ben (Feb 25 2025 at 20:41):

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?

view this post on Zulip Brendan Hansknecht (Feb 25 2025 at 21:30):

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.

view this post on Zulip Richard Feldman (Feb 25 2025 at 21:37):

a way to think of braces is that they're a way to add statements to an expression

view this post on Zulip Richard Feldman (Feb 25 2025 at 21:38):

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
}

view this post on Zulip Richard Feldman (Feb 25 2025 at 21:38):

so that whole braces-enclosed thing is an expression

view this post on Zulip Brendan Hansknecht (Feb 25 2025 at 21:43):

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)
    }
}

view this post on Zulip Richard Feldman (Feb 25 2025 at 21:44):

yep!

view this post on Zulip Richard Feldman (Feb 25 2025 at 21:44):

I was writing up docs for them and I think :point_up: is the actual definition we want

view this post on Zulip Richard Feldman (Feb 25 2025 at 21:46):

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."

view this post on Zulip Richard Feldman (Feb 25 2025 at 21:47):

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)

view this post on Zulip Richard Feldman (Feb 25 2025 at 21:48):

and that's it, that's the complete explanation of how { ... } blocks work

view this post on Zulip Richard Feldman (Feb 25 2025 at 21:48):

(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)

view this post on Zulip Anthony Bullard (Feb 26 2025 at 14:18):

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

view this post on Zulip Anthony Bullard (Feb 26 2025 at 14:19):

So you can pass them as function args, have them as list items even have them as the predicate of an if

view this post on Zulip Brendan Hansknecht (Feb 26 2025 at 18:07):

Function args?

view this post on Zulip Brendan Hansknecht (Feb 26 2025 at 18:08):

Haha

view this post on Zulip Brendan Hansknecht (Feb 26 2025 at 18:08):

That sounds like terrible syntax, but I guess it makes sense it can work anywhere

view this post on Zulip Anthony Bullard (Feb 26 2025 at 18:53):

Yeah don’t do it, but you could

view this post on Zulip Brendan Hansknecht (Feb 26 2025 at 19:23):

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)
})

view this post on Zulip Richard Feldman (Feb 26 2025 at 19:34):

yeah I think the main benefit is just the conceptual simplicity of the rule

view this post on Zulip Richard Feldman (Feb 26 2025 at 19:35):

so it's easier to understand how it works and what you can do with it

view this post on Zulip Brendan Hansknecht (Feb 26 2025 at 19:40):

Yep

view this post on Zulip Simon Taeter (Feb 27 2025 at 11:59):

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?

view this post on Zulip Anthony Bullard (Feb 27 2025 at 12:05):

We talked about that, but it ends up being semantic overload on ()....it would be apply args, tuples, parenthesized expressions, AND blocks.

view this post on Zulip Anthony Bullard (Feb 27 2025 at 12:05):

The last two have a lot of contention

view this post on Zulip jan kili (Feb 27 2025 at 15:55):

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
    )

view this post on Zulip Kiryl Dziamura (Feb 27 2025 at 16:01):

I just realized. the following is possible as well, right? kinda funny but why not :D

x = 42 - { 21 * 2 }

view this post on Zulip Kiryl Dziamura (Feb 27 2025 at 16:08):

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

view this post on Zulip jan kili (Feb 27 2025 at 16:21):

Maybe the formatter should convert braces to parens if they only contain a single expression.

view this post on Zulip jan kili (Feb 27 2025 at 16:22):

(and then it can do the existing pass to remove excess parens)

view this post on Zulip Anthony Bullard (Feb 27 2025 at 16:41):

Yes a block with a single expression and no newlines/comments will have the braces removed by the formatter

view this post on Zulip Richard Feldman (Feb 27 2025 at 16:44):

unless it's if a { b } else { c }, right? :big_smile:

view this post on Zulip Kiryl Dziamura (Feb 27 2025 at 16:51):

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

view this post on Zulip Richard Feldman (Feb 27 2025 at 17:31):

I think it's just a formatting thing

view this post on Zulip Richard Feldman (Feb 27 2025 at 17:31):

for if only

view this post on Zulip Derin Eryilmaz (Feb 27 2025 at 18:49):

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.

view this post on Zulip Simon Taeter (Mar 02 2025 at 10:01):

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.

view this post on Zulip Sam Mohr (Mar 02 2025 at 10:02):

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

view this post on Zulip Sam Mohr (Mar 02 2025 at 10:03):

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

view this post on Zulip Sam Mohr (Mar 02 2025 at 10:03):

If it was a drop in readability, then we'd want to maybe reconsider more strongly

view this post on Zulip Brendan Hansknecht (Mar 02 2025 at 18:36):

Note: we also have an opinionated formatter that will stop the super giant single line of code thing.

view this post on Zulip Luke Boswell (Jul 09 2025 at 22:49):

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.

view this post on Zulip Anthony Bullard (Jul 09 2025 at 22:55):

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

view this post on Zulip Anthony Bullard (Jul 09 2025 at 22:56):

i think then is helpful, or some other change is needed

view this post on Zulip Anthony Bullard (Jul 09 2025 at 22:56):

Like require then if the next expression isn't a block

view this post on Zulip Brendan Hansknecht (Jul 09 2025 at 23:27):

This is why python changes the order, right?
1 if foo else 2

view this post on Zulip Anthony Bullard (Jul 10 2025 at 00:03):

probably

view this post on Zulip Anthony Bullard (Jul 10 2025 at 00:03):

it seems the human needs a keyword or punctuation even if the machine doesn't :rolling_on_the_floor_laughing:

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:42):

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 then and I see other people making the same mistake.

is that because you're used to the old Roc syntax though?

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:44):

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

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:45):

if lack of curly braces is the problem, we can just have a convention of putting curly braces around the branches, right?

view this post on Zulip Anthony Bullard (Jul 10 2025 at 00:45):

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

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:45):

sure, but both of those Just Work, right?

view this post on Zulip Anthony Bullard (Jul 10 2025 at 00:45):

yep

view this post on Zulip Anthony Bullard (Jul 10 2025 at 00:45):

the formatter currently doesn't have that style

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:46):

right, but to me this seems like a formatter question and not a syntax design question

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:46):

like I don't see a case for reintroducing then or switching to 1 if foo or anything like that

view this post on Zulip Anthony Bullard (Jul 10 2025 at 00:46):

so maybe the solve is just to have it output braces

view this post on Zulip Anthony Bullard (Jul 10 2025 at 00:46):

That's probably right

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:46):

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

view this post on Zulip Anthony Bullard (Jul 10 2025 at 00:46):

As long as the thing most people will do just works, then it's all about the conventional style

view this post on Zulip Anthony Bullard (Jul 10 2025 at 00:47):

And formatter

view this post on Zulip Anthony Bullard (Jul 10 2025 at 00:47):

it's nice that people can write a C style if statement and it'll just work

view this post on Zulip Anthony Bullard (Jul 10 2025 at 00:48):

And we probably will turn it at worst to a Go-style if statement

view this post on Zulip Anthony Bullard (Jul 10 2025 at 00:48):

in the formatter

view this post on Zulip Anthony Bullard (Jul 10 2025 at 00:48):

assuming we unwrap useless parenthesized expressions

view this post on Zulip Brendan Hansknecht (Jul 10 2025 at 00:49):

I think it does feel odd due to feeling like a record

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:49):

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

view this post on Zulip Brendan Hansknecht (Jul 10 2025 at 00:49):

Also, does any language have this syntax?

view this post on Zulip Anthony Bullard (Jul 10 2025 at 00:49):

But we should keep {}s i think

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:49):

Brendan Hansknecht said:

Also, does any language have this syntax?

with the curly braces it is 100% exactly how Rust does it

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:50):

including the branches being expressions

view this post on Zulip Brendan Hansknecht (Jul 10 2025 at 00:50):

Ah yeah. They just always require braces and we don't?

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:50):

e.g. this is valid Rust code that does the same thing as what it would do in Roc:

if foo {
    a
} else {
    b
}

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:50):

right

view this post on Zulip Brendan Hansknecht (Jul 10 2025 at 00:50):

Ok

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:51):

so if the formatter adds braces, then it's just Rust code

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:51):

honestly my only hesitation for requiring that the formatter add braces is that it creates an inconsistency

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:52):

without special-casing if, the rule can be consistently:

view this post on Zulip Richard Feldman (Jul 10 2025 at 00:52):

if foo { 1 } else { 2 }

uses braces but is single-line, so the simple formatter rule of "braces == multiline" would turn this into multiline

view this post on Zulip Luke Boswell (Jul 10 2025 at 00:52):

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.

view this post on Zulip Luke Boswell (Jul 10 2025 at 00:55):

Ironically Rust and Zig don't have then... so I'm definitely getting this from some place else

view this post on Zulip Luke Boswell (Jul 10 2025 at 00:57):

It must be current Roc's influence... or maybe even I'm having a stroke and think I'm writing VB

view this post on Zulip Tobias Steckenborn (Jul 10 2025 at 02:18):

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