Stream: ideas

Topic: ✔ insignificant whitespace


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

the idea has been brought up before about giving Roc a grammar where whitespace is no longer significant, and in the past the idea hasn't gotten much traction because of how different it makes Roc code look

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

I realized there might be a way to get the syntax delta very small. I'm probably missing some things, but I hadn't seen it discussed before, so I thought I'd at least bring it up and see if it's a potentially simpler change than it's seemed in the past :big_smile:

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

the basic idea is:

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

going through the realworld code base, it seems like the only delta is adding end at the end of a couple of conditionals and loops, and nothing else is affected. Importantly, list.map(|arg| arg + 1) is unaffected because we're already using parens to delimit there.

this would make nested defs require an end or parens, but those are super uncommon anyway.

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

which feels like a pretty small delta to get the benefits of:

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

that said, maybe I'm missing something!

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

curious what others think of the idea in general, and also whether I've missed something

view this post on Zulip Niclas Ahden (Feb 09 2025 at 23:45):

Totally disregarding technical pros/cons: I would prefer end-less ifs and whens. I just started moving projects from WSA to PNC and found that the LOC grows noticeably. Adding ends will add a few more LOC. This means that less code is visible on the screen at a time, and that makes it harder for me (personally) to reason about code.

I usually have a few files open, plus a watcher or two (e.g. server and client recompilation). All-in-all screen real estate is at a premium. If I can't see all relevant code at the same time, then I have a harder time thinking about a problem.

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

here's the full delta in roc-realworld: https://github.com/rtfeldman/roc-realworld/compare/end

view this post on Zulip Sam Mohr (Feb 09 2025 at 23:53):

Wow, 5 lines!

view this post on Zulip Sam Mohr (Feb 09 2025 at 23:53):

When you put it like that...

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

it only needed 5 ends across about 1k lines of code

view this post on Zulip Luke Boswell (Feb 09 2025 at 23:54):

Sounds like a pretty major upside... with very minor downside

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

almost all of the places where it would have been needed happened to already be inside parens or at the end of top-level decls

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

Sam Mohr said:

When you put it like that...

yeah I was surprised too!

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

I wish I'd thought of this years ago :sweat_smile:

view this post on Zulip Luke Boswell (Feb 09 2025 at 23:55):

Is the rule confusing around when end is required and when not, i.e. inside parens

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

I figure if someone puts it inside parens, we can just accept it and warn that it's unnecessary and then have the formatter drop it

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

so they'd be short-lived haha

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

@Niclas Ahden what do you think in the context of the LOC delta being this small? e.g. 5 lines out of ~1k is about 0.5% increase

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

(no idea how much that would or wouldn't generalize though!)

view this post on Zulip Niclas Ahden (Feb 09 2025 at 23:57):

I worry that it wouldn't generalize well

view this post on Zulip Niclas Ahden (Feb 09 2025 at 23:59):

Separately, looking at the diff I think the code is strictly worse, as only noise was added (to me). Someone else might find that the ends are useful, but to me they are simply a chore I have to add for no benefit.

This sounds super negative, so I want to stress this isn't the end of the world or anything (I've written my fair share of Ruby). My wording above is just "straight", not upset.

view this post on Zulip Niclas Ahden (Feb 10 2025 at 00:08):

Reading the project I see that the style is a bit different than mine. Most uses of when and if are at the end of a function, which is why this project only had a +5 diff. It may well be that I could implement a similar style, and it'd be a general improvement, but at the moment my projects have more uses of when and if in the middle of functions than this project does.

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

interesting! anything you can share?

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

hm, I think if we had the "top-level must be indented some amount, and that's it" rule, then end could also be omitted if the thing right after it is an assignment :thinking:

that could reduce the realworld example from 5 ends to 3

view this post on Zulip Brendan Hansknecht (Feb 10 2025 at 00:19):

I'm pretty sure I would rather have the significant whitespace than a rule like this. That or just commit and give me standard {} braces.

view this post on Zulip Brendan Hansknecht (Feb 10 2025 at 00:20):

I have always dislike end keywords though. And I think making it a rare exception just make it even weirder when you need it. I think it would be inconsistent and more annoying to apply.

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

here it is with parens instead of end https://github.com/rtfeldman/roc-realworld/compare/parens

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

and the rule I mentioned about top-level decls still needing indents

view this post on Zulip Brendan Hansknecht (Feb 10 2025 at 00:27):

Yeah, I prefer that.

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

ok actually I realized the one place where it was necessary after that "top-level must be indented" rule was a nested def that didn't need to be nested

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

so I updated it and now it's just rearranging things and the only place that needs parens is the for loop

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

and everything else doesn't need any other changes

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

that is extremely unexpected :sweat_smile:

view this post on Zulip Niclas Ahden (Feb 10 2025 at 00:54):

Richard Feldman said:

interesting! anything you can share?

Sadly, I can't without asking for permission because, happily, most of my Roc is client work! :) But essentially: when I write functions that cover a high-level workflow (e.g. create_facebook_ad! or merge_chunks_into_file!) I like to have all branching in that function, and very little branching in the functions that actually do stuff. That way I end up with a single high-level function that describes the workflow clearly, and logs everything, and a bunch of small functions that do stuff. Does that make sense? The high-level functions can end up with six branches or something, so to me that's a lot of ends.

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

no worries - could you maybe make one of them generic? (or ask Claude to make it generic :big_smile:) so we could see the shape?

view this post on Zulip Kevin Gillette (Feb 10 2025 at 01:01):

Richard Feldman said:

Yes, Python is mainstream, but as we know, there are plenty of people who hate significant whitespace but there aren't people who hate insignificant whitespace :big_smile:

Some people who love Python do dislike insignificant whitespace :shrug:

view this post on Zulip Kevin Gillette (Feb 10 2025 at 01:04):

Richard Feldman said:

almost all of the places where it would have been needed happened to already be inside parens or at the end of top-level decls

It does feel syntactically inconsistent to allow end to be omitted in some cases. I could see that causing confusion, or perhaps leading some people to always add parens to force one rule (or never using parens to force the other rule).

view this post on Zulip Niclas Ahden (Feb 10 2025 at 01:20):

Richard Feldman said:

no worries - could you maybe make one of them generic? (or ask Claude to make it generic :big_smile:) so we could see the shape?

I rewrote a part from memory (pseudo-Roc). This is an example where all logging and decision making is in one function, which, to me, makes it easy to understand the whole workflow and gives me an opportunity to explain it with comments like this at a high level. The functions that do the work (verify_file_size!, merge_chunks!) have little branching and probably no logging, just useful tagged errors.

merge_chunks_into_final_file! = |...|
  info!(job_id, "Merging chunks for ...")

  if verify_file_size!(final_file_path, expected_file_size) then
      info!(job_id, "Chunks of ... has already been merged into ...")
      return Ok(final_file_path)

  verify_total_size_of_chunks!(...)?

  when timed!(merge_chunks!(...)) is
        Ok((time_taken, final_file_path)) ->
            info!(job_id, "Merging chunks into \"${final_file_path}\" took ${time_taken}.")
            Ok(final_file_path)
        Err(e) ->
            info!(job_id, "Cleaning up failed merge ...")

            when delete_file!(...) is
              Ok(_) ->
                  info!(job_id, "Deleted output of failed merge ...")
              Err(e) ->
                  # It's OK if this file isn't successfully removed. Either it doesn't exist, or it'll be
                  # cleaned up later by our cron jobs anyway. This _could_ result in files lingering and eating
                  # up all disk space, but that's an acceptable risk (low probability, low impact, and easily
                  # detectable).
                  warn!(job_id, "Failed to delete output of failed merge ...")

            Err(...)

  # ... verify the file size of the merged file

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

ok nice!

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

one idea: we could say that return "ends a block"

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

because in this position, if return were not ending the if, it would just be creating dead code after it

merge_chunks_into_final_file! = |...|
  if verify_file_size!(final_file_path, expected_file_size) then
      info!(job_id, "Chunks of ... has already been merged into ...")
      return Ok(final_file_path)

  verify_total_size_of_chunks!(...)?

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

and a warning

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

so instead we could interpret that as ending the block, and leave it as-is

view this post on Zulip Richard Feldman (Feb 10 2025 at 01:24):

in that case, I think the only change would be that the nested when would need parens around it?

view this post on Zulip Sam Mohr (Feb 10 2025 at 01:24):

I'd definitely prefer end over parens if we go this route.

view this post on Zulip Sam Mohr (Feb 10 2025 at 01:25):

Anything we can do to avoid nested parens is ideal

view this post on Zulip Niclas Ahden (Feb 10 2025 at 01:27):

Richard Feldman said:

in that case, I think the only change would be that the nested when would need parens around it?

The function continues a bit further but I just didn't write it out (see ending comment) so the when timed!(merge_chunks!(...)) is would also need parens or end. I can't recall exactly how long this "workflow" is but it at least verifies the file size, so there's at least one more branch before it's done.

view this post on Zulip Sam Mohr (Feb 10 2025 at 01:30):

One way to reduce nesting on this if we go for end is my favorite drum to beat, the Kotlin last-arg-is-a-function-gets-pulled-out drum:

walk_items = |items|
    List.walk(items, { count: 0 }) |state, item|
        amount = add_up(item)
        { count: state.count + amount }
    end

view this post on Zulip Sam Mohr (Feb 10 2025 at 01:30):

Which is a reason I'm a proponent of this proposal

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

let's not bring that into scope for this :sweat_smile:

could be a separate discussion, but the main thing I'm thinking about here is potentially addressing the downsides of significant whitespace rather than the potential upsides

view this post on Zulip Niclas Ahden (Feb 10 2025 at 01:44):

@Richard Feldman These kinds of functions is where I would expect the ends to appear most often. They usually have 4-6 branches. I like to write these functions right away when implementing a new feature, kind of like writing a story to figure out what I need to do. Therefore I have quite a few of them. Perhaps I shouldn't do this, but it's a habit I got since rspec I think. Hope this input helps! I'm off to bed now at 02:44 :joy:

view this post on Zulip Richard Feldman (Feb 10 2025 at 01:45):

thanks!

view this post on Zulip Richard Feldman (Feb 10 2025 at 01:51):

to summarize, the revised idea is:

view this post on Zulip Richard Feldman (Feb 10 2025 at 01:51):

cc @Joshua Warner and @Anthony Bullard since I know you've both had thoughts on whitespace significance in the past!

view this post on Zulip Richard Feldman (Feb 10 2025 at 01:56):

https://github.com/rtfeldman/roc-realworld/compare/parens is an example of the realworld app with these rules apply, and the only place they come up is one for loop (after rearranging a few lines)

view this post on Zulip Anthony Bullard (Feb 10 2025 at 02:24):

Ok, I think my brain is melting right now

view this post on Zulip Anthony Bullard (Feb 10 2025 at 02:24):

I'll probably have to come back and ask questions about this in the morning

view this post on Zulip Anthony Bullard (Feb 10 2025 at 02:24):

And I'm sure Joshua will have to as well

view this post on Zulip Joshua Warner (Feb 10 2025 at 02:32):

Making the top-level a special case feels very weird to me

view this post on Zulip Joshua Warner (Feb 10 2025 at 02:32):

Either whitespace-based scoping should work everywhere or it should work nowhere.

view this post on Zulip Joshua Warner (Feb 10 2025 at 02:34):

I want to be able to copy-paste declarations from the top level to an inner level freely without having to change any extra things (besides indentation, obviously)

view this post on Zulip Luke Boswell (Feb 10 2025 at 02:50):

I like the simplification of for ... in ( ... )

view this post on Zulip Richard Feldman (Feb 10 2025 at 02:59):

Joshua Warner said:

Making the top-level a special case feels very weird to me
Either whitespace-based scoping should work everywhere or it should work nowhere.

I think without that, semicolons would be necessary after statements :thinking:

view this post on Zulip Richard Feldman (Feb 10 2025 at 03:05):

well, either that or end being necessary after top-level declarations as well as ending if/when/etc.

view this post on Zulip Brendan Hansknecht (Feb 10 2025 at 03:07):

or parens/brackets of some from around them

view this post on Zulip Richard Feldman (Feb 10 2025 at 03:07):

yeah

view this post on Zulip Joshua Warner (Feb 10 2025 at 03:07):

I would say that all of those options sound preferable to the inconsistent top level declaration rule

view this post on Zulip Richard Feldman (Feb 10 2025 at 03:07):

really? haha

view this post on Zulip Joshua Warner (Feb 10 2025 at 03:08):

Hmm maybe I’m misunderstanding your proposal?

view this post on Zulip Richard Feldman (Feb 10 2025 at 03:09):

well with top-level decls still needing to be indented, the delta for the 1k LoC roc-realworld is just adding either parens or end around a single for statement

view this post on Zulip Richard Feldman (Feb 10 2025 at 03:09):

any of those alternatives would be much more invasive

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

Having a clean and consistent syntax seems more important than making the delta to the existing syntax smaller, no? 

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

those two seem to be in tension though

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

adding a bunch of symbols all over the place would definitely seem to make the syntax less clean

view this post on Zulip Joshua Warner (Feb 10 2025 at 03:13):

Making the rules different for the top level and nested levels makes the syntax even less clean IMO

view this post on Zulip Joshua Warner (Feb 10 2025 at 03:14):

One of the things that I really like about roc is the lambda syntax is exactly the same as the function declaration syntax. This seems to be going away from that spirit.

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

hm, actually statements don't come up in that code base

view this post on Zulip Richard Feldman (Feb 10 2025 at 03:30):

so I guess the rule could be that ending a line in ; is syntax sugar for beginning it with _ =

view this post on Zulip Richard Feldman (Feb 10 2025 at 03:38):

that would come up pretty often in rocci bird

view this post on Zulip Richard Feldman (Feb 10 2025 at 03:42):

but that does seem to be the only way rocci bird (773 LoC) would be affected

view this post on Zulip Richard Feldman (Feb 10 2025 at 03:43):

@Joshua Warner what do you think about multiline strings?

view this post on Zulip Joshua Warner (Feb 10 2025 at 03:44):

From a parsing and formatting perspective, I would like to get rid of them ;)

view this post on Zulip Joshua Warner (Feb 10 2025 at 03:44):

Make a lot of things unnecessarily more difficult

view this post on Zulip Richard Feldman (Feb 10 2025 at 03:46):

so, an alternative design for them would be something like:

my_multiline_str =
    """line 1
    """line 2
    """line 3

view this post on Zulip Richard Feldman (Feb 10 2025 at 03:46):

but that makes pasting a lot less convenient

view this post on Zulip Richard Feldman (Feb 10 2025 at 03:47):

it does have the upside of making the parser more error-tolerant though

view this post on Zulip Richard Feldman (Feb 10 2025 at 03:52):

can you think of any reason this design wouldn't work?

view this post on Zulip Joshua Warner (Feb 10 2025 at 03:53):

Ooh yeah that makes things easier on the lexing and parsing side. Formatting is probably a wash in terms of complexity.

view this post on Zulip Joshua Warner (Feb 10 2025 at 03:54):

With that change I think you could almost lex in parallel if you wanted

view this post on Zulip Joshua Warner (Feb 10 2025 at 03:55):

Eg split the file into n chunks and lex on separate threads. Just need to find the nearest line boundary.

view this post on Zulip Joshua Warner (Feb 10 2025 at 03:56):

You’d have to also require that string interpolation expressions not contain new lines

view this post on Zulip Joshua Warner (Feb 10 2025 at 04:14):

FWIW I have long thought about suggesting the zig syntax for multiline strings as an alternative. But I like the """ prefix better than the \\ prefix.

view this post on Zulip Richard Feldman (Feb 10 2025 at 04:20):

hm I think actually all inline defs would need to end in ; in this idea

edit: nm, I realized that's not necessary!

view this post on Zulip Richard Feldman (Feb 10 2025 at 04:20):

I think the """" on each line" design makes more sense in a world where we support importing files as strings

view this post on Zulip Richard Feldman (Feb 10 2025 at 04:25):

because if you really want to paste something big, that might be the nicer option anyway

view this post on Zulip Richard Feldman (Feb 10 2025 at 04:26):

in fact, we could consider not supporting """ at first and see if there's still demand given import-as-string :thinking:

view this post on Zulip Joshua Warner (Feb 10 2025 at 04:34):

import-as-string doesn't solve the case of wanting to do string interpolation to a large string

view this post on Zulip Richard Feldman (Feb 10 2025 at 04:34):

ah good point

view this post on Zulip Richard Feldman (Feb 10 2025 at 04:34):

unless we wanted to make it fancier :stuck_out_tongue:

view this post on Zulip Richard Feldman (Feb 10 2025 at 04:35):

but that's neither here nor there haha

view this post on Zulip Joshua Warner (Feb 10 2025 at 04:35):

Haha I was thinking exactly the same thing. Something something roc-template-language

view this post on Zulip Joshua Warner (Feb 10 2025 at 04:35):

Anyway

view this post on Zulip Richard Feldman (Feb 10 2025 at 04:36):

point is, there are multiple paths we could take to get to not taking indentation into account at all, if desired

view this post on Zulip Richard Feldman (Feb 10 2025 at 04:49):

I guess a use for ; would be if-without-else:

if foo then
   bar!();

something

edit: never mind, that wouldn't work!

view this post on Zulip Kilian Vounckx (Feb 10 2025 at 07:02):

I don't really have a strong preference. Just wanted to say that ocaml treats local defs and top-level ones differently IIRC. Local ones need the in keyword after definition. Top-levels (optionally) need ;;. I don't know if that's a good or a bad thing. But there is precedent at least

view this post on Zulip Eli Dowling (Feb 10 2025 at 07:44):

I always use ;; between functions in ocaml because otherwise the errors can get pretty wild when the syntax is wonky. Like including the next top level inside the current one.

view this post on Zulip Jasper Woudenberg (Feb 10 2025 at 07:56):

Even with explicit end delimiters we will still use whitespace to format code. Humans are using the whitespace for parsing the structure of the code, while the compiler will use delimiters. I think this leads to problems when whitespace and delimiters don't agree.

I've definitely ran into errors with delimited languages where the parser is complaining an end-delimiter is missing, but it can't tell me where, and I have to manually comb through a large body of code to check if everything is properly delimited.

Or put differently, I think the happy path for delimiters is nice (like the copy/paste benefits), but the failure path isn't.

Whereas for whitespace-sensitivity, the failure path seems much nicer. In most cases, bad indentation leads to the parser misinterpretting the structure of the code instead of the parser failing. I think that's actually a benefit, because it means you can run the formatter to make the problem obvious.

And where whitespace-sensitivity cannot be resolved automatically, for instance the mixed spaces/tab case, it's possible to give a pretty nice and clear error message to the user.

view this post on Zulip Anthony Bullard (Feb 10 2025 at 11:31):

Richard Feldman said:

well, either that or end being necessary after top-level declarations as well as ending if/when/etc.

Do this and change static dispatch from . to : and Roc begins its Lua story arc.

view this post on Zulip Jonathan (Feb 10 2025 at 11:31):

Jasper Woudenberg said:

Humans are using the whitespace for parsing the structure of the code, while the compiler will use delimiters [...]

I'd like to just add a potentially silly perspective here that I _really_ appreciate consistent delimiters, particularly in more expression orientated languages, because it kind of 'wraps' up the value inside. For instance, it reinforces that an if-block is an expression, with a result that can be assigned. I think an 'explicitly' terminated block is also better at communicating intent, perhaps in a similar way to how brackets could provide visual clarification in PNC.

Richard Feldman said:

As terminating delimiters primarily provide me with consistency and visual clues, I feel that optional or case specific uses would nullify much of the benefit, as well as contribute to syntax complexity/weirdness (though this could not be a problem in practice!!).

Jasper Woudenberg said:

[...] while the compiler will use delimiters. I think this leads to problems when whitespace and delimiters don't agree.

I've definitely ran into errors with delimited languages where the parser is complaining an end-delimiter is missing, but it can't tell me where, and I have to manually comb through a large body of code to check if everything is properly delimited.

This happens frequently for me in Elixir, but I mind it less than having (with whitespace-significance) syntactically valid code behaving incorrectly due to whitespace. If I am capable of missing an end or } then I think I am equally capable of missing/adding an indent, particularly if copy/pasting.

Granted, I have experienced this mainly in a dynamic statement-orientated language (Python) and so the feedback loop may be much shorter if the compiler can catch incorrect indentation errors inferred through mismatching types, rather than the often unhelpful mis-matching delimiter error. However, if the syntax is capable of being whitespace insensitive whilst not requiring a terminator (i.e. as Richard described), wouldn't that imply such cases of mis-matching delimiter can be caught precisely, due to the delimiter's redundancy?

I suppose this is mainly one more vote on both Brendan and Kevin's comments, but I wanted to add my opinion on benefits to clarity.

view this post on Zulip Anthony Bullard (Feb 10 2025 at 11:33):

The big issue here is that Roc decided to support statements - i.e., expressions that occur mid-block that aren't in a definition

view this post on Zulip Anthony Bullard (Feb 10 2025 at 11:34):

In a expression based language if you don't have statements you can always say that a block ends when you see a trailing expression

view this post on Zulip Anthony Bullard (Feb 10 2025 at 11:36):

Using fn(); as syntax sugar for _= fn() Makes a sort of sense. As it is just a small interruption in the block. But man man man do I hate semicolons

view this post on Zulip Jasper Woudenberg (Feb 10 2025 at 11:46):

Jonathan said:

Granted, I have experienced this mainly in a dynamic statement-orientated language (Python) and so the feedback loop may be much shorter if the compiler can catch incorrect indentation errors inferred through mismatching types, rather than the often unhelpful mis-matching delimiter error.

Yeah, I think this checks out, at least in my personal experience with Elm and Haskell.

Jonathan said:

However, if the syntax is capable of being whitespace insensitive whilst not requiring a terminator (i.e. as Richard described), wouldn't that imply such cases of mis-matching delimiter can be caught precisely, due to the delimiter's redundancy?

That's an interesting approach. In terms of cost/benefit this is the same as having whitespace-sensitivity without delimiters :thinking:.

I wonder if an editor integration would be an alternative in this case: it could draw 'block end' delimiters in the right places, giving a visual cue without needing to type it out.

view this post on Zulip Jonathan (Feb 10 2025 at 12:03):

Jasper Woudenberg said:

I wonder if an editor integration would be an alternative in this case: it could draw 'block end' delimiters in the right places, giving a visual cue without needing to type it out.

I used to use something like that for Python (repo, image from repo) and found it was tricky to balance visual noise and the clarity it provided, but did help somewhat, though not providing quite the same functionality as my favorite associated editor feature which is highlighting/selecting/jumping to matching parens. Veering off topic however - apologies.

To throw spaghetti at the wall - there's another (I think unserious) idea along these lines that delimiters could be optional but added by a formatter, much like optional ";;" in OCaml.

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

Anthony Bullard said:

The big issue here is that Roc decided to support statements - i.e., expressions that occur mid-block that aren't in a definition

this is really just purity inference introducing useful functions that return {}.

if you take out the concept of statements, the same design problem still exists, you just have no option but to write _ = or {} = in order to call those functions :big_smile:

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

Jasper Woudenberg said:

I've definitely ran into errors with delimited languages where the parser is complaining an end-delimiter is missing, but it can't tell me where, and I have to manually comb through a large body of code to check if everything is properly delimited.

Or put differently, I think the happy path for delimiters is nice (like the copy/paste benefits), but the failure path isn't.

Whereas for whitespace-sensitivity, the failure path seems much nicer. In most cases, bad indentation leads to the parser misinterpretting the structure of the code instead of the parser failing. I think that's actually a benefit, because it means you can run the formatter to make the problem obvious.

I've had the same experience, although I don't know of any parsers that track indentation level and use it only to inform error messages (and/or the formatter).

obviously doing that would make them more complex than they'd otherwise have to be though!

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

I do think just running the formatter can help with the error cases though - if it reorganizes my code in a way that's surprising to me, then I can see what the compiler thinks the structure of the code is, and fix it accordingly.

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

I think this may be one of the few syntax threads whether every combination of preference has been expressed :laughing:

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

so we should definitely keep the status quo, and also change it, and the thing we should change it to is every different option.

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

I don't feel like I understand where this thread stands, unless I take your summary above as the definitive statement Richard

view this post on Zulip Richard Feldman (Feb 10 2025 at 12:59):

I don't understand where it stands either haha

view this post on Zulip Anthony Bullard (Feb 10 2025 at 13:02):

I have proposed end before, I really like it and some well-loved languages have it - Ruby, Elixir, and Lua are some examples. I really hate significant whitespace and it's one of my biggest annoyances with the ML-derived FP world. I think a grammar that has fully enclosed nodes is just easier to parse for _all_ computers and _most_ humans. But I recognize that the self-selected group of people who use this language today either like whitespace significance, or don't mind it. (Again, I really hate it but grin and bear it do to the semantics of the language).

view this post on Zulip Richard Feldman (Feb 10 2025 at 13:04):

so basically we've seen:

view this post on Zulip Anthony Bullard (Feb 10 2025 at 13:05):

I will say that if supporting ; for statements (null defs) is really ALL we need to get rid of whitespace significance I could support that

view this post on Zulip Anthony Bullard (Feb 10 2025 at 13:07):

Because at that point we can treat these null def statements as being like every other def, and therefore a block can be a series of defs followed by an expr and that resolves block delimiting issues

view this post on Zulip Anthony Bullard (Feb 10 2025 at 13:08):

I would include the multiline string syntax change with this

view this post on Zulip Richard Feldman (Feb 10 2025 at 13:08):

yeah it's almost just that, except nested when still needs something extra (could just be parens)

view this post on Zulip Richard Feldman (Feb 10 2025 at 13:08):

and nested defs, although those are fairly optional imo

view this post on Zulip Richard Feldman (Feb 10 2025 at 13:09):

Anthony Bullard said:

I would include the multiline string syntax change with this

yeah I'm treating that as separate; feels like there are several reasonable options there

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

Why does nested when need something extra? Its grammar is:

"when" EXPR "is" [PATTERN "->" BLOCK]*

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

Where block is:

[DECL]* EXPR

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

Seems to be very well defined and fully delimited unless I'm missing something

view this post on Zulip Richard Feldman (Feb 10 2025 at 13:12):

when foo is
    Bar ->
        when x is
            Baz -> 0
            Blah -> 1
    Stuff -> 2

view this post on Zulip Richard Feldman (Feb 10 2025 at 13:12):

which when does Stuff -> 2 go with?

view this post on Zulip Richard Feldman (Feb 10 2025 at 13:12):

(unless we use parens or end to disambiguate)

view this post on Zulip Richard Feldman (Feb 10 2025 at 13:12):

(or significant indentation)

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

Oh shoot, of course

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

So a when in the final position of a block inside of a when

view this post on Zulip Anthony Bullard (Feb 10 2025 at 13:14):

It seems that is rare enough to make requiring parens to be reasonable

view this post on Zulip Anthony Bullard (Feb 10 2025 at 13:14):

Even if I think that having end would be more consistent

view this post on Zulip Jonathan (Feb 10 2025 at 13:42):

Richard Feldman said:

To refine what I said (and rescind a bit of support for "parens everywhere" haha): I don't see it as a negative to have scoping delimiters, especially if already committing to PNC, plus often finding them useful. But I don't think that is a majority opinion, especially in a community that enjoys/enjoyed WSA.

If it's just one (or two?) case that requires disambiguation however? Semi-colons for statements, combined with wrapping the whole nested when in brackets seems like a great solution imo (rather than introducing a new end keyword for a rare case, or adding parens for more/all blocks).

view this post on Zulip Jasper Woudenberg (Feb 10 2025 at 13:42):

I'm curious, do we agree there's ambiguity in the syntax either way?

I think by looking at how these ambiguities impact errors, there's the possibility to draw the conversation away from personal tastes.

view this post on Zulip Richard Feldman (Feb 10 2025 at 13:53):

I don't think there's ambiguity in significant indentation - we just say that only tab characters are allowed for indentation

view this post on Zulip Richard Feldman (Feb 10 2025 at 13:54):

the only reason to consider both tabs and spaces would be for error tolerance/recovery

view this post on Zulip Richard Feldman (Feb 10 2025 at 13:55):

an upside of significant indentation seems to be that if you get the code to "look right" in terms of indentation, it'll probably do what you expect

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

whereas with whitespace-insensivity, if you forget a semicolon or nest things in a particular way, it can "look right" but be broken

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

unless you require a lot more explicit delimiters than we do today

view this post on Zulip Richard Feldman (Feb 10 2025 at 14:05):

e.g. braces around every block, or end after every block like Ruby

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

Richard Feldman said:

e.g. braces around every block, or end after every block like Ruby

This (end after every block that doesn't have a clear delimiter in the gramar already) would be my personal preference if we did away with whitespace significance. But I would be fine with some delimiter just being used for when combined with ; for null defs. I'm also OK with the status quo, though I'd like to go forward with the proposed new multiline string syntax

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:29):

Just a quick thought - wouldn't requiring when clauses to have trailing commas on non-trailing branches accomplish the same thing? I guess it would be inconsistent with all other comma-delimited items in the grammar though as otherwise we would allow a trailing comma...

Actually it doesn't even help that trailing nested when case

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:35):

I really think when...is is the only existing syntactic construction that would require an end

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:36):

Obviously for will need it as well when it's implemented

view this post on Zulip Richard Feldman (Feb 10 2025 at 14:38):

yeah, either end or surrounding it in parens

(and only nested whens would need that treatment)

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:38):

And then you only need to track newlines for the purposes of ending a statement in the parser. The formatter could become easier if it only looked for a trailing comma OR newline in collections and complex expressions

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:39):

I thought end is nice since:

A) it reduces paren overload
B) The rest of the syntax for the construction is keyword driven

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:40):

But yeah I agree it could be an optional piece of syntax for explicit delimiting

view this post on Zulip Richard Feldman (Feb 10 2025 at 14:40):

yeah, a note about braces and record punning before I forget:

foo(|x| { x })

if braces delimit blocks, then would this be equivalent to |x| { x: x } or |x| x?

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:41):

Don't do this

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:41):

JS has this exact problem and it is MISERABLE

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:41):

I literally used to hit this at least once or twice a day

view this post on Zulip Richard Feldman (Feb 10 2025 at 14:41):

yeah I don't think we should do braces, I'm just bringing it up because they've been suggested before as being the most mainstream option (e.g. it's what Gleam uses)

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:42):

It's very similar to Zig's "expected a , after statement" when you forget the . in front of an anonymous struct literal

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:43):

Braces with implicit return means it would have to be

foo(|x| { { x } })

To return a record

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:43):

foo(|x| { x })

returns x

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:43):

Yes I agree - no way I'm down with that

view this post on Zulip Richard Feldman (Feb 10 2025 at 14:44):

I'm not sure how confusing/surprising the experience would be when forgetting the edge case of nested when

view this post on Zulip Richard Feldman (Feb 10 2025 at 14:44):

e.g. if I write this:

when foo is
    Bar ->
        when x is
            Baz -> 0
            Blah -> 1
    Stuff -> 2

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:44):

I would 100000000000% prefer the world where when (and for eventually) requires end

view this post on Zulip Richard Feldman (Feb 10 2025 at 14:44):

the formatter would change it to:

when foo is
    Bar ->
        when x is
            Baz -> 0
            Blah -> 1
            Stuff -> 2

view this post on Zulip Richard Feldman (Feb 10 2025 at 14:44):

so I think I'd see the problem pretty quickly

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:44):

The formatter would definitely tell you what the problem is :rofl:

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:44):

And more than likely there would be one or more type errors as well

view this post on Zulip Richard Feldman (Feb 10 2025 at 14:45):

right, although if I'm not using the formatter, I might not understand the type error

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:45):

foo not being handled exhaustively, and x being matched against a bigger union than expected

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:45):

For sure

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:45):

I think we could probably detect this case and make the error more helpful

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:46):

But it wouldn't be all cases especially with type inference and no annotations

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:46):

This is why I'm for just for a little more typing to make it 100% consistent

view this post on Zulip Richard Feldman (Feb 10 2025 at 14:46):

actually the type error might be fine if it showed both branches in the snippet

view this post on Zulip Richard Feldman (Feb 10 2025 at 14:47):

e.g. "these last two branches don't agree":

        when x is
            …
            Blah -> 1
    Stuff -> 2

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:47):

Yeah, possibly

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:47):

Just like a -C1 in grep

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:47):

A little context helps :-)

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:49):

I think showing up to the previous and the next non-blank line would be helpful 98% of the time

view this post on Zulip jan kili (Feb 10 2025 at 14:51):

As someone who loves significant whitespace well-indented code and always liked Roc's spacey aesthetic, this topic jump-scared me :joy: I'll try to keep an open mind, though!

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:53):

But as long as we don't end up with this:

(when foo is (
    (Bar (when x is (
        (Baz  (0))
        (Blah (1)))))
    (Stuff (2))))

:rofl::rofl::rofl::rofl::rofl::rofl::rofl::rofl:

view this post on Zulip jan kili (Feb 10 2025 at 14:54):

Would the phrase "whitespace is now insignificant in Roc" be a literal/accurate one? Does that include newlines? Would the formatter still apply one true indentation for each line?

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:55):

"Whitespace insignificance" typically means "indentation significance"

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:56):

Not that no whitespace matters at all

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:57):

I think I've learned from Chakra that you want to always at the very least have the invariant that all non expr statements and all blocks end in a newline

view this post on Zulip Anthony Bullard (Feb 10 2025 at 14:57):

And a good formatter should probably have some hard and fast rules about when a statement becomes multiline

view this post on Zulip Richard Feldman (Feb 10 2025 at 14:58):

here's what it would look like if all whens and ifs ended in end: https://github.com/rtfeldman/roc-realworld/compare/end-always

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

I'm gonna catch some heat for this opinion, but I think with syntax highlighting that would look HOT

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

And that is all the disambiguation we need right? We don't need lambda bodies to have end?

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

So we don't go full Ruby/Lua/Elixir?

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

that's correct, as long as we're okay with {} = or ; for statements

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

if we want to avoid that, then we need end for all blocks, including lambdas

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

I actually personally like that version too

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

I like that it makes the when and if look more like a single expression because it's not indented more at the end than the start

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

especially since when gets double-indented

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

I also like where the comma ends up on this part of a multiline record:

       auth:
            when Env.var("DB_PASSWORD") is
                Ok(password) -> Password password
                Err(VarNotFound) -> None
            end,

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

today it's:

       auth:
            when Env.var("DB_PASSWORD") is
                Ok(password) -> Password password
                Err(VarNotFound) -> None,

which has always felt awkward to me :sweat_smile:

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

Sweet, I can deal with a semicolon if I need to do a Stdout.line!

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

The _only_ bad part about that is that means that ; would be introduced pretty early in the tutorial

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

an advantage of "always end a when" would be that the example @Niclas Ahden gave earlier (of using when in the middle of some statements), it wouldn't look or feel unusual

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

although it would come with more ends, which Niclas pointed out are noisy compared to today, and @Brendan Hansknecht expressed a general dislike for :sweat_smile:

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

FWIW I'd prefer if we never drop _ =s

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

I always feel icky doing when inside of another expression today

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

JanCVanB said:

FWIW I'd prefer if we never drop _ =s

I wouldn't hate this either

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

I think it muddies the mental model for very little gain

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

we could certainly try it

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

could end up with a lot of _ = log.err!("this should never happen") :sweat_smile:

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

Wait, as opposed to what?

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

as opposed to log.err!("this should never happen");

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

or {} = log.err!("this should never happen")

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

Love the first one!

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

Niclas's example without ; or statements:

merge_chunks_into_final_file! = |...|
  _ = info!(job_id, "Merging chunks for ...")

  if verify_file_size!(final_file_path, expected_file_size) then
      _ = info!(job_id, "Chunks of ... has already been merged into ...")
      return Ok(final_file_path)

  _ = verify_total_size_of_chunks!(...)?

  when timed!(merge_chunks!(...)) is
        Ok((time_taken, final_file_path)) ->
            _ = info!(job_id, "Merging chunks into \"${final_file_path}\" took ${time_taken}.")
            Ok(final_file_path)
        Err(e) ->
            _ = info!(job_id, "Cleaning up failed merge ...")

            _ = when delete_file!(...) is
              Ok(_) ->
                  info!(job_id, "Deleted output of failed merge ...")
              Err(e) ->
                  # It's OK if this file isn't successfully removed. Either it doesn't exist, or it'll be
                  # cleaned up later by our cron jobs anyway. This _could_ result in files lingering and eating
                  # up all disk space, but that's an acceptable risk (low probability, low impact, and easily
                  # detectable).
                  warn!(job_id, "Failed to delete output of failed merge ...")
            end

            Err(...)
  end

  # ... verify the file size of the merged file

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

Versus:

merge_chunks_into_final_file! = |...|
  info!(job_id, "Merging chunks for ...");

  if verify_file_size!(final_file_path, expected_file_size) then
      info!(job_id, "Chunks of ... has already been merged into ...");
      return Ok(final_file_path)

  verify_total_size_of_chunks!(...)?;

  when timed!(merge_chunks!(...)) is
        Ok((time_taken, final_file_path)) ->
            info!(job_id, "Merging chunks into \"${final_file_path}\" took ${time_taken}.");
            Ok(final_file_path)
        Err(e) ->
            info!(job_id, "Cleaning up failed merge ...");

            when delete_file!(...) is
              Ok(_) ->
                  info!(job_id, "Deleted output of failed merge ...")
              Err(e) ->
                  # It's OK if this file isn't successfully removed. Either it doesn't exist, or it'll be
                  # cleaned up later by our cron jobs anyway. This _could_ result in files lingering and eating
                  # up all disk space, but that's an acceptable risk (low probability, low impact, and easily
                  # detectable).
                  warn!(job_id, "Failed to delete output of failed merge ...")
            end;

            Err(...)
  end

  # ... verify the file size of the merged file

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

If we add whends, I'd prefer the former to the latter.

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

some rocci bird code in the _ = and end style:

run_title_screen! : TitleScreenState => Model
run_title_screen! = |prev|
    state = { prev &
        rocci_idle_anim: update_animation(prev.frame_count, prev.rocci_idle_anim)
    }
    _ = set_text_colors!()
    _ = W4.text!("Rocci Bird!!!", { x: 32, y: 12 })
    _ = W4.text!("Click to start!", { x: 24, y: 72 })
    _ = draw_ground!(ground_sprite, 0)
    _ = draw_plants!(plant_sprite_sheet, state.plants)

    shift = idle_shift(state.frame_count, state.rocci_idle_anim)
    draw_animation!(state.rocci_idle_anim, { x: player_x, y: player_start_y + shift })
    gamepad = W4.get_gamepad!(Player1)
    mouse = W4.get_mouse!()

    start = gamepad.button1 or gamepad.up or mouse.left

    if start then
        init_game(state)
    else
        TitleScreen(state)
    end

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

with {} = instead:

run_title_screen! : TitleScreenState => Model
run_title_screen! = |prev|
    state = { prev &
        rocci_idle_anim: update_animation(prev.frame_count, prev.rocci_idle_anim)
    }
    {} = set_text_colors!()
    {} = W4.text!("Rocci Bird!!!", { x: 32, y: 12 })
    {} = W4.text!("Click to start!", { x: 24, y: 72 })
    {} = draw_ground!(ground_sprite, 0)
    {} = draw_plants!(plant_sprite_sheet, state.plants)

    shift = idle_shift(state.frame_count, state.rocci_idle_anim)
    draw_animation!(state.rocci_idle_anim, { x: player_x, y: player_start_y + shift })
    gamepad = W4.get_gamepad!(Player1)
    mouse = W4.get_mouse!()

    start = gamepad.button1 or gamepad.up or mouse.left

    if start then
        init_game(state)
    else
        TitleScreen(state)
    end

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

with ; instead:

run_title_screen! : TitleScreenState => Model
run_title_screen! = |prev|
    state = { prev &
        rocci_idle_anim: update_animation(prev.frame_count, prev.rocci_idle_anim)
    }
    set_text_colors!();
    W4.text!("Rocci Bird!!!", { x: 32, y: 12 });
    W4.text!("Click to start!", { x: 24, y: 72 });
    draw_ground!(ground_sprite, 0);
    draw_plants!(plant_sprite_sheet, state.plants);

    shift = idle_shift(state.frame_count, state.rocci_idle_anim)
    draw_animation!(state.rocci_idle_anim, { x: player_x, y: player_start_y + shift })
    gamepad = W4.get_gamepad!(Player1)
    mouse = W4.get_mouse!()

    start = gamepad.button1 or gamepad.up or mouse.left

    if start then
        init_game(state)
    else
        TitleScreen(state)
    end

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

If we add thends, I'd prefer the former to the latters.

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

I don't mind how _ = looks in these examples, but I don't like that I can't tell whether it's discarding output or not

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

like with {} = I know it's not discarding information, but with _ = it might be

view this post on Zulip Jonathan (Feb 10 2025 at 15:15):

I thought the semicolons would make the intent a bit less clear, but given that statements are always impure (?), the lack of assignment in combination with a ! for me communicates better than _ =because the latter implies a possible error code, as opposed to a side effect and ~no return value

ie what Richard said but less clearly haha

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

Can't believe I'm saying this, but I think I prefer ; here

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

Richard Feldman said:

like with {} = I know it's not discarding information, but with _ = it might be

That's fair - I care more about keeping the = consistency than the _ use specifically.

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

This is the one I'm not sure I love:

verify_total_size_of_chunks!(...)?;

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

I don't mind that, although maybe that's because I'm used to it from Rust

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

I'm not a fan of semicolons in general, but there's something I do appreciate about their only being used for a statement which runs a side effect and discards its output, which is the most imperative thing possible (and which is also what I associate semicolons with)

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

it's sort of like "hey this is full-on imperative right here"

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

"we have entered the Imperative Zone"

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

What about do as a keyword? It could be a single expression, or multiline with an end keyword (this block can have no defs and has no return value). It's syntactic sugar.

Singleline:

do verify_total_size_of_chunks!(...)?

Multiline:

run_title_screen! : TitleScreenState => Model
run_title_screen! = |prev|
    state = { prev &
        rocci_idle_anim: update_animation(prev.frame_count, prev.rocci_idle_anim)
    }
    do
        set_text_colors!()
        W4.text!("Rocci Bird!!!", { x: 32, y: 12 })
        W4.text!("Click to start!", { x: 24, y: 72 })
        draw_ground!(ground_sprite, 0)
        draw_plants!(plant_sprite_sheet, state.plants)
    end

    shift = idle_shift(state.frame_count, state.rocci_idle_anim)
    draw_animation!(state.rocci_idle_anim, { x: player_x, y: player_start_y + shift })
    gamepad = W4.get_gamepad!(Player1)
    mouse = W4.get_mouse!()

    start = gamepad.button1 or gamepad.up or mouse.left

    if start then
        init_game(state)
    else
        TitleScreen(state)
    end

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

Regarding the naming of that keyword, I expect folks to expect that to act like Haskell's do

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

do could alternatively mean "this is a delimited block, where statements can exist alongside other things but can't return anything"

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

It pretty similar to do in haskell, where you do all of your IO code

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

I think syntactically it looks lovely though

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

But in Roc, any foo! can do IO, regardless of its return value. if start! then can do IO.

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

I think the more common case would be logging

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

Niclas' example again using do:

merge_chunks_into_final_file! = |...|
  info!(job_id, "Merging chunks for ...");

  if verify_file_size!(final_file_path, expected_file_size) then
      do info!(job_id, "Chunks of ... has already been merged into ...")
      return Ok(final_file_path)

  verify_total_size_of_chunks!(...)?;

  when timed!(merge_chunks!(...)) is
        Ok((time_taken, final_file_path)) ->
            do info!(job_id, "Merging chunks into \"${final_file_path}\" took ${time_taken}.")
            Ok(final_file_path)
        Err(e) ->
            do info!(job_id, "Cleaning up failed merge ...")

            do when delete_file!(...) is
              Ok(_) ->
                  info!(job_id, "Deleted output of failed merge ...")
              Err(e) ->
                  # It's OK if this file isn't successfully removed. Either it doesn't exist, or it'll be
                  # cleaned up later by our cron jobs anyway. This _could_ result in files lingering and eating
                  # up all disk space, but that's an acceptable risk (low probability, low impact, and easily
                  # detectable).
                  warn!(job_id, "Failed to delete output of failed merge ...")
            end

            Err(...)
  end

  # ... verify the file size of the merged file

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

where you just have one statement and then you move on

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

Example from tutorial:

app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }

import pf.Stdout
import pf.Stdin

main! = |_args|
    do Stdout.line!("Type in something and press Enter:")?
    input = Stdin.line!({})?
    Stdout.line!("Your input was: ${input}") # do here would probably be OK?

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

Richard I was thinking that do as a block could also be used for for

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

And since for is basically a statement (are we planning on a return value from it?), it would require a do block

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

JanCVanB said:

Would the phrase "whitespace is now insignificant in Roc" be a literal/accurate one? Does that include newlines? Would the formatter still apply one true indentation for each line?

Soon when we're not in a creative keyword brainstorming flow, I'd like to revisit the motivations for this, since this sounds like it's to facilitate arbitrary indentation but I highly value authoritatively-indented code.

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

I think to allow someone to copy/paste code around without worrying about it , in the spirit of "inform, but never block"

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

I argue that even if Roc needed zero syntax changes to "make indentation insignificant", we shouldn't do it.

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

...unless we're only talking about expanding the formatter's input domain without expanding its output range.

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

That's fair but "inform, never block" is kind of our motto, and whitespace significance can often be quite the little dictator in terms of blocking in some really hard to notice cases

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

Unless we dictate that the tokenizer will only accept tabs as starting whitespace on a line - but even then copy paste still fails annoyingly often in this case

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

Are you using the phrase "whitespace significance" as a synonym for "pre-formatted indentation parsing"?

view this post on Zulip Artur Domurad (Feb 10 2025 at 15:54):

hmm, I'm not sure I'm buying the "easier to copy and paste" motivation for this change.
I think roc works really great at the moment without adding a lot of "noise" in shape of end and ;
But to be fair, I did not do a lot of copying and pasting in roc...

And I would say that end is a lot more strange in a language than indent based blocks.

view this post on Zulip Jasper Woudenberg (Feb 10 2025 at 16:00):

Sorry to come back to this, and if I missed the answer in a previous comment. I really like the 'never block' philosophy, but I'm wondering if adding end-delimiter is swapping one kind of blocking behavior for another: Wouldn't the parser have to block on a missing end?

view this post on Zulip jan kili (Feb 10 2025 at 16:13):

My questions are about what happens when someone writes/formats/compiles/distributes Roc code that isn't well-indented (which all of the above examples are).

Will the formatter indent this input line?

main! = |_args|
    Stdout.line!("Type in something and press Enter:")?;
input = Stdin.line!({})?
    Stdout.line!("Your input was: ${input}")

Let's add a mid-block newline, which is common in Roc for visual separation. Is this a valid single function def?

main! = |_args|
    Stdout.line!("Type in something and press Enter:")?;

input = Stdin.line!({})?
    Stdout.line!("Your input was: ${input}")

Should all of the examples in above messages compile with all indentation deleted?

Do semicolons/ends enable mylib.min.roc files with zero newlines, tabs, or multispaces?

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

Will the formatter indent this input line?

Under the semicolon (or end) proposal, this sort of thing would be allowed + automatically fixed up, yes.

I think we could also do something like emit a (non-blocking) warning about indentation being funky, which may help someone catch a stray ;.

view this post on Zulip jan kili (Feb 10 2025 at 16:24):

Does that mean that of the following possible goals

  1. make whitespace insignificant in Roc
  2. make indentation insignificant in Roc
  3. make whitespace insignificant in Roc's formatter
  4. make indentation insignificant in Roc's formatter
  5. make whitespace insignificant in Roc's formatter's input
  6. make indentation insignificant in Roc's formatter's input

this topic is only discussing goal 6?

view this post on Zulip jan kili (Feb 10 2025 at 16:34):

Or are we using a specific technical definition of "insignificant" that differs from the casual meaning of "arbitrary" / "anything goes" / "personal preference"?

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

the formatter would 100% enforce indentation no matter what

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

there's no world where it preserves whatever arbitrary indentation you chose

view this post on Zulip Sam Mohr (Feb 10 2025 at 16:38):

I believe "insignificant" here means "the amount of whitespace doesn't matter, just if there's at least one character of extra whitespace on this line compared to the last one"

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

right

view this post on Zulip jan kili (Feb 10 2025 at 16:39):

Where 2 spaces > 1 tab?

view this post on Zulip Sam Mohr (Feb 10 2025 at 16:40):

No, where "if line one is space + tab + space, line two needs to be space + tab + space + space/tab"

view this post on Zulip Sam Mohr (Feb 10 2025 at 16:41):

For stuff like when

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

a better way to think of it is that the parser discards 100% of indenting information (because it's not significant anymore), and it's like every line has been trimmed...and then the formatter inserts indentations from scratch to make it look nice

view this post on Zulip Sam Mohr (Feb 10 2025 at 16:41):

Maybe that's out of date from prior discussion in this thread, though

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:42):

so it doesn't matter if you use tabs, spaces, how many you use, etc. - it all gets discarded, and then the formatter inserts new indentations from scratch

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:42):

(in this proposed design)

view this post on Zulip jan kili (Feb 10 2025 at 16:45):

Does that mean that an equivalent topic name would be "fully automating indentation"?

view this post on Zulip jan kili (Feb 10 2025 at 16:47):

To me, languages with "insignificant whitespace" are JSON and Java(almost).

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:48):

Artur Domurad said:

And I would say that end is a lot more strange in a language than indent based blocks.

hm, I don't know if that's true.

among mainstream languages, only Python uses significant indentation and only Ruby uses end

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:48):

Python is more popular than Ruby, but also way more people complain about its syntax

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:49):

the most popular non-mainstream language which uses either is Elixir, which uses end, and the most popular non-mainstream language with significant indentation is Haskell

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

as with other topics, I don't think that should be the main consideration, but if we're just looking at the one isolated attribute of "which one makes the language appealing to more programmers, end or significant indentation" I think the evidence suggests it's end

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:52):

as an aside, I haven't thought deeply about this question, but I think it's possible that the proposed design doesn't care about newlines either

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:52):

aside from string literals needing to be single-line, as they are in all mainstream languages

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:53):

like for example I think this would parse:

x = when foo is A -> 1 B -> 2 C -> 3 end

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:53):

I'm not saying we should encourage writing code like that, but I think the parser would be able to understand it and then the formatter could output it differently

view this post on Zulip jan kili (Feb 10 2025 at 16:54):

All of my fears are eliminated. Thank you.

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:56):

here is a potentially interesting way to think about the do idea: what if do .. end is syntax sugar for ( ... )?

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:57):

in that case, I think do could be an alternative to semicolon, as suggested above - e.g.

my_fn = |arg|
    do log.err!("this should never happen")
    x = arg + 1
    x * 2

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:57):

so here, instead of end we have the x = serving as a block-ender

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:58):

and then we have:

for x in y do
    whatever!()
end

...being equivalent to:

for x in y (
    whatever!()
)

view this post on Zulip jan kili (Feb 10 2025 at 16:58):

Is that just to avoid {} =? I like {} =.

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:58):

and then also you could use it for nested defs:

x = do
    y = whatever * 2
    y + 1
end

...as opposed to:

x = (
    y = whatever * 2
    y + 1
)

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:59):

JanCVanB said:

Is that just to avoid {} =? I like {} =.

yeah, it would be an alternative to {} =

view this post on Zulip Richard Feldman (Feb 10 2025 at 16:59):

so far I've seen more opposition to {} = than support for it, but I could be wrong :big_smile:

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

unlike _ =, we could make there be a warning for do Stdout.line!("...") in the case where you're ignoring some useful output

view this post on Zulip jan kili (Feb 10 2025 at 17:02):

Would this generalize to do1+2end*do3-4end?

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

compared to the {} = example

run_title_screen! : TitleScreenState => Model
run_title_screen! = |prev|
    state = { prev &
        rocci_idle_anim: update_animation(prev.frame_count, prev.rocci_idle_anim)
    }
    do set_text_colors!()
    do W4.text!("Rocci Bird!!!", { x: 32, y: 12 })
    do W4.text!("Click to start!", { x: 24, y: 72 })
    do draw_ground!(ground_sprite, 0)
    do draw_plants!(plant_sprite_sheet, state.plants)

    shift = idle_shift(state.frame_count, state.rocci_idle_anim)
    draw_animation!(state.rocci_idle_anim, { x: player_x, y: player_start_y + shift })
    gamepad = W4.get_gamepad!(Player1)
    mouse = W4.get_mouse!()

    start = gamepad.button1 or gamepad.up or mouse.left

    if start then
        init_game(state)
    else
        TitleScreen(state)
    end

view this post on Zulip Niclas Ahden (Feb 10 2025 at 17:03):

Richard Feldman said:

as with other topics, I don't think that should be the main consideration, but if we're just looking at the one isolated attribute of "which one makes the language appealing to more programmers, end or significant indentation" I think the evidence suggests it's end

Evidence suggesting that significant indention is the winner: Those drawn to Haskell and Elm often cite conciseness and clarity as strong positives. There's a "feeling" in those languages that people like, and I think a lot of it is the "light-weightedness" of high signal and low noise. Just like the "feeling" or Rust is more verbosity, bulkier, detail-oriented etc. The fact that Elixir uses end is, to my knowledge, to appeal to Ruby programmers, not an explicit choice saying "this is clearly better". Elixir was (is?) marketed as "Ruby programmer? Come get some BEAM, it's awesome!"

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

JanCVanB said:

Would this generalize to do1+2end*do3-4end?

do would be a keyword, so you would need some whitespace - becuase do1 would be a variable name

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

Niclas Ahden said:

Richard Feldman said:

as with other topics, I don't think that should be the main consideration, but if we're just looking at the one isolated attribute of "which one makes the language appealing to more programmers, end or significant indentation" I think the evidence suggests it's end

Evidence suggesting that significant indention is the winner: Those drawn to Haskell and Elm often cite conciseness and clarity as strong positives. There's a "feeling" in those languages that people like, and I think a lot of it is the "light-weightedness" of high signal and low noise. Just like the "feeling" or Rust is more verbosity, bulkier, detail-oriented etc. The fact that Elixir uses end is, to my knowledge, to appeal to Ruby programmers, not an explicit choice saying "this is clearly better".

I think that's a fair summary of the general controversy around significant indentation:

view this post on Zulip Niclas Ahden (Feb 10 2025 at 17:05):

Agreed, and so, I can't agree with "I think the evidence suggests it's end"

PS. Again, I hope my assertions are read as light-heartedly as I intend them. I have a tendency to say exactly what I mean, which can come across strong in text.

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

I think it's a subtle distinction, but I don't think the responses are balanced here

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

to give an example, the first conference talk I ever gave was about functional programming in CoffeeScript (which was mainstream at the time).

I had a slide giving a brief overfiew of CoffeeScript syntax for those unfamiliar, and when I started talking about significant whitespace, there was a guy in the front row who hissed every time I mentioned significant indentation, and then after the third hiss he stood up and walked out of the room

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

I'm not saying I'm like scarred from that experience or anything, but I do think that's an example of how skewed the reactions are. A lot of people really strongly dislike significant indentation, to a degree that I haven't seen with other mainstream syntax.

actually, the thing that reminded me to revisit this topic is that I was watching an interview on a podcast, and the guest randomly mentioned something like "the compiler I was working on didn't support significant indentation, because that is insane and I don't understand how any language can possibly consider that reasonable, but anyway..." - like he went way out of his way to hate on significant indentation even though nobody had even brought it up

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

on a personal level I disagree with him, but the reason I say that it's less mainstream is just that some percentage of programmers seem to feel super strongly about this, in a way that they don't about alternatives

view this post on Zulip jan kili (Feb 10 2025 at 17:16):

Richard Feldman said:

I think that's a fair summary of the general controversy around significant indentation:

It seems like, in Roc, the meaning of code dictates its indentation. I can't tell if that would make those opponents happy or hiss.

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

like I said, this isn't the only factor, or even the main factor, that we should use to decide what to do here, but I'm pretty convinced that the scales are tipped against significant indentation when it comes to adoption

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

from what I've seen, the specific thing that people strongly dislike is literally just changing indentation changing the meaning of the code in any way

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

I don't think the justification matters to them :big_smile:

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

they see it as unjustifiable at face value

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

in contrast, I don't see people having this visceral reaction to thinks like end or braces

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

the worst-case scenario I've seen is a mild-to-moderate preference for something else

view this post on Zulip Sam Mohr (Feb 10 2025 at 17:20):

Richard Feldman said:

there was a guy in the front row who hissed every time I mentioned significant indentation

bro

view this post on Zulip jan kili (Feb 10 2025 at 17:21):

I often read those hisses as indicative of a "I don't like languages enforcing coding formatting style", which I'm firmly against catering to.

view this post on Zulip Sam Mohr (Feb 10 2025 at 17:21):

Enforcing coding style seems to work well for go

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

Sam Mohr said:

Richard Feldman said:

there was a guy in the front row who hissed every time I mentioned significant indentation

bro

even funnier, later on I was chatting with a group of people and he walked up and started talking to us like nothing had happened. I don't think he remembered me as the speaker he had hissed at and walked out on.

view this post on Zulip Niclas Ahden (Feb 10 2025 at 17:23):

I see your point. I would argue that people coming to Elm from JavaScript loved it though. I posit that Elm would have had wild success if progress would have continued at the rate it did in 0.14-0.19. Sadly, for many reasons, it didn't, but I think Elm really drew people in and I often heard praise of its signal v. noise/no cruft. I see this as a "some people like X, others Y" and it's one of the things I like in Roc. I lost WSA, but am OK with PNC due to SD. There was talk of parens in types. Now we're discussing losing indentation significance. It feels like we're slowly moving to look like Rust (perhaps because we're all using that, and we're used to it, so we're skewed to make those suggestions and accept them?)

Again, I would survive if indentation significance went away, but I'm laying out the whole thing to explain the perspective.

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

yeah I totally hear that!

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

to me, changing to braces is what feels like the "bridge too far" to me :stuck_out_tongue:

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

partly because of the ambiguity drawbacks we talked about earlier, but also just because it would feel like changing the look/feel of the language primarily to look more mainstream, despite the downsides

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

something I appreciate about the do + end version of the idea is that it feels like it adds a few more keywords here and there, which is objectively more verbose, but even setting aside the copy/paste benefits (etc.) I do like some aspects of how it looks with end

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

also, I'm actually curious to hear other perspectives on this, but in general I've mostly heard neutral to positive things about Ruby syntax in comparison to the more typical braces-and-semicolons syntax

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

as in, not only does it seem to be uncontroversial (I don't really hear people saying "I have a significant preference for C-style syntax over Ruby-style syntax"), but I also hear a good number of people expressing a mild to strong preference for Ruby

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

I do hear some people preferring Python/Haskell/Elm-style syntax over Ruby-style (and over C-style), but then again there's a significant controversy there that I don't see with Ruby syntax

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

all of which is to say, I've always had kind of a heuristic of "doing what Ruby does is always a reasonable choice" when it comes to syntax

view this post on Zulip jan kili (Feb 10 2025 at 17:32):

I feel like this topic has a spicy name and several sensible ideas. Shall we split into four-ish perpendicular discussions, like we did for static dispatch?

view this post on Zulip Niclas Ahden (Feb 10 2025 at 17:32):

If anything, I think people say that Ruby looks "scripty". But that seems like a mild opinion :shrug:

view this post on Zulip jan kili (Feb 10 2025 at 17:38):

I see 3 distinct types of end above - whend & thend for enabling auto-indentation and doend for reducing parentheses.

view this post on Zulip jan kili (Feb 10 2025 at 17:39):

There's also do for reducing {} =s.

view this post on Zulip jan kili (Feb 10 2025 at 17:40):

There's also ; for reducing {} =s.

view this post on Zulip jan kili (Feb 10 2025 at 17:42):

There's also alternative syntaxes for multiline strings, for enabling auto-indentation.

view this post on Zulip jan kili (Feb 10 2025 at 17:45):

There's also requiring a block to explicitly end (with/without introducing end) for enabling auto-indentation.

view this post on Zulip Joshua Warner (Feb 10 2025 at 17:45):

What does whend and thend mean? Are you saying those are keywords?

view this post on Zulip jan kili (Feb 10 2025 at 17:46):

I'm being silly and contracting:

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

I just think it would be good to write down a concrete proposal for "Remove Significant Whitespace from Roc"

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

High-level:

  1. All whens must end in end.
  2. All ifs must end in end.
  3. Statement expressions will only be allowed in a block-trailing position
  4. A block will always be any sequence of definitions followed by a single expression
  5. The parser will not pay attention to newlines or comments at all.
  6. The formatter will use whitespace to determine when an expression and/or it's containing statement is single or multiline, and whether an additional blank should be inserted (as well as comments).

For current usages of statement expressions mid-block there are some alternatives

do syntax:

  1. do is available as syntactic sugar over {} = EXPR
  2. do followed by block completed with end is sugar for any number of statements where any statement that is not an definition is wrapped in a {} = ... decl and those null definitions will be lowered into the enclosing block.
  3. dos are a valid statement

; syntax:

  1. ; is a statement terminator that is syntactic sugar for a statement expression, converting it to {} = EXPR
  2. It is only valid for a mid-block statement expression

Disallow mid-block statement expressions:

  1. Remove this from the grammar, forcing all current usages to move to {} =

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

Valid top level statements are IMPORT, EXPECT, and DEFINITION.
Valid statements in a block are DEFINITION, DO/';', and EXPR, EXPECT, RETURN, CRASH.
Valid statements in a do..end block (if do syntax is chosen) are EXPR, RETURN, EXPECT, CRASH.

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

Questions I have:

  1. Is a do valid inside a do...end block?
  2. Is control flow expression valid inside a do...end block?

view this post on Zulip Jonathan (Feb 10 2025 at 18:17):

Anthony Bullard said:

High-level:

  1. All whens must end in end.
  2. All ifs must end in end.
  3. do is available as syntactic sugar over {} = EXPR
  4. do followed by block completed with end is sugar for any number of statements where any statement that is not an definition is wrapped in a {} = ... decl and those null definitions will be lowered into the enclosing block.
  5. dos are a valid statement
  6. A block will always be any sequence of definitions followed by a single expression
  7. The parser will not pay attention to newlines or comments at all.
  8. The formatter will use whitespace to determine when an expression and/or it's containing statement is single or multiline, and whether an additional blank should be inserted (as well as comments).

Is that consistent with dos being syntax sugar for brackets? Or is this use case for do-as-brackets truly only for a series of defs followed by an expression? (In which case they aren't strictly the same as brackets?)

Richard Feldman said:

and then also you could use it for nested defs:

x = do
    y = whatever * 2
    y + 1
end

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

That's a great question!

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

I don't think I understand why that would be needed

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

But Richard always thinks of a case I'm not thinking of

view this post on Zulip jan kili (Feb 10 2025 at 18:22):

Anthony Bullard said:

High-level:

...

  1. do is available as syntactic sugar over {} = EXPR
  2. do followed by block completed with end is sugar for any number of statements where any statement that is not an definition is wrapped in a {} = ... decl and those null definitions will be lowered into the enclosing block.
  3. dos are a valid statement
    ...

The majority of my point on topic splitting is that 3-5 seem unrelated to (merely inspired by) "insignificant whitespace" (what I'm trying to reframe as "fully-automated indentation").

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

But with end on if and when (and I would say expect as wel), all expressions are fully delimited, so there should be no need for that

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

JanCVanB said:

Anthony Bullard said:

High-level:

...

  1. do is available as syntactic sugar over {} = EXPR
  2. do followed by block completed with end is sugar for any number of statements where any statement that is not an definition is wrapped in a {} = ... decl and those null definitions will be lowered into the enclosing block.
  3. dos are a valid statement
    ...

The majority of my point on topic splitting is that 3-5 seem unrelated to (merely inspired by) "insignificant whitespace" (what I'm trying to reframe as "fully-automated indentation").

I think you are wrong here. But I get that there is the alternative of using either ; or just no longer allowing mid-block statement expressions and forcing people to use {} =

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

I'll clarify the above post

view this post on Zulip jan kili (Feb 10 2025 at 18:24):

Ah, yes, I'm wrong - I keep forgetting those =-less expressions are allowed and I dislike them.

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

I think Richard likes having that syntax quite a bit

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

Jonathan said:

Is that consistent with dos being syntax sugar for brackets? Or is this use case for do-as-brackets truly only for a series of defs followed by an expression? (In which case they aren't strictly the same as brackets?)

Richard Feldman said:

and then also you could use it for nested defs:

Ruby x = do y = whatever * 2 y + 1 end

I think this would be useful for a function where you want to be free to use mid-block statement expression, e.g, Richard's rocci bird example:

run_title_screen! : TitleScreenState => Model
run_title_screen! = |prev| do
    state = { prev &
        rocci_idle_anim: update_animation(prev.frame_count, prev.rocci_idle_anim)
    }
    set_text_colors!()
    W4.text!("Rocci Bird!!!", { x: 32, y: 12 })
    W4.text!("Click to start!", { x: 24, y: 72 })
    draw_ground!(ground_sprite, 0)
    draw_plants!(plant_sprite_sheet, state.plants)

    shift = idle_shift(state.frame_count, state.rocci_idle_anim)
    draw_animation!(state.rocci_idle_anim, { x: player_x, y: player_start_y + shift })
    gamepad = W4.get_gamepad!(Player1)
    mouse = W4.get_mouse!()

    start = gamepad.button1 or gamepad.up or mouse.left

    if start then
        init_game(state)
    else
        TitleScreen(state)
    end
end

Instead of wrapping just the "region" with statements expression, you wrap the entire body of the top-level function

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

This would be the easiest way to format existing code that has mid-block statement expressions. If a block has one, wrap it in do...end

view this post on Zulip Jonathan (Feb 10 2025 at 18:33):

Would one be free to not use do (for instance around that entire function body) and continue using _ = for statement expressions?

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

Of course, do is pure sugar

view this post on Zulip Jonathan (Feb 10 2025 at 18:34):

I mean essentially in terms of style tastes, this does allow those such as @JanCVanB to continue using their preferred syntax.

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

And I think we wouldn't format that away (except possibly through a flag), but I might be wrong. Richard has stated in the past a preference for a zero-config formatter

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

So it just depends on if the formatter will be made to be "opinionated" about this

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

To allow myself a personal opinion, I think the above is very nice looking and very consistent and very unamibiguous.

view this post on Zulip jan kili (Feb 10 2025 at 18:39):

I wonder if there's a single-character version of {} = that many folks would prefer...

! = W4.text!("Click to start!", ...

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

I just don't like useless assignments

view this post on Zulip jan kili (Feb 10 2025 at 18:41):

Part of the joy of FP to me is that everything is assignments.

view this post on Zulip jan kili (Feb 10 2025 at 18:44):

I'm just thinking about how to make it visually easiest for eyes to parse, and I like having the anchor of the equals sign on every expression. Maybe I just haven't read enough effectful Roc code to get used to not seeing them consistently!

view this post on Zulip Jonathan (Feb 10 2025 at 18:45):

JanCVanB said:

Part of the joy of FP to me is that everything is assignments.

Doesn't that go out of the window though once you accept statement-like semantics + purity inference? You'd just be assigning the result of something that is meaningless so it looks more FP? I do understand the feeling though.

view this post on Zulip jan kili (Feb 10 2025 at 18:47):

I like that ! draws attention to effectful code, and I feel like a function returning nothing deserves similar attention, rather than effectively hiding it. However, that might be out of scope for this discussion.

view this post on Zulip Sam Mohr (Feb 10 2025 at 18:49):

Does do Stdout.line!("...") not imply that to you?

view this post on Zulip jan kili (Feb 10 2025 at 18:51):

I actually like that, hence my insistence that do is a separate proposal from do/end :laughing:

view this post on Zulip Sam Mohr (Feb 10 2025 at 18:54):

dewend

view this post on Zulip jan kili (Feb 10 2025 at 18:55):

endo

view this post on Zulip jan kili (Feb 10 2025 at 18:56):

do/od :laughing:

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

I expect any of these dos would pair naturally with adding a void return type.

view this post on Zulip Sam Mohr (Feb 10 2025 at 19:06):

That doesn't seem necessary, seems like {} does the job quite well

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

I say the same about {} = and feel alone :laughing:

view this post on Zulip Richard Feldman (Feb 10 2025 at 19:33):

something I'd like to get to (which I don't think we've quite arrived at yet) is a design for this where I can just sort of get into a habit for how to write certain constructs and have it just work everywhere such that I don't have to think about it

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

like ideally I want to just get in the habit of either always writing do for each statement, or ; (or whatever) and not have to decide whether to wrap a series of them in a do ... end so I can avoid the individual do or ; on each of them

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

so maybe the do ... end idea is a mistake

view this post on Zulip Richard Feldman (Feb 10 2025 at 19:42):

looking at the semicolons version, it feels like the semicolons are pretty easy to miss

view this post on Zulip Richard Feldman (Feb 10 2025 at 19:43):

I can imagine a lot of " :man_facepalming: I forgot a semicolon" experiences when writing statements

view this post on Zulip Richard Feldman (Feb 10 2025 at 19:45):

I guess another option we haven't talked about is something like let or def for local constants

run_title_screen! : TitleScreenState => Model
run_title_screen! = |prev|
    def state = { prev &
        rocci_idle_anim: update_animation(prev.frame_count, prev.rocci_idle_anim)
    }
    set_text_colors!()
    W4.text!("Rocci Bird!!!", { x: 32, y: 12 })
    W4.text!("Click to start!", { x: 24, y: 72 })
    draw_ground!(ground_sprite, 0)
    draw_plants!(plant_sprite_sheet, state.plants)

    def shift = idle_shift(state.frame_count, state.rocci_idle_anim)
    draw_animation!(state.rocci_idle_anim, { x: player_x, y: player_start_y + shift })
    def gamepad = W4.get_gamepad!(Player1)
    def mouse = W4.get_mouse!()

    def start = gamepad.button1 or gamepad.up or mouse.left

    if start then
        init_game(state)
    else
        TitleScreen(state)
    end

view this post on Zulip Richard Feldman (Feb 10 2025 at 19:48):

I think that works because it makes the syntax for top-level defs different, so you can tell when you've hit the next top-level def

view this post on Zulip Richard Feldman (Feb 10 2025 at 19:49):

that said, I definitely prefer not having to write def or let :sweat_smile:

view this post on Zulip Richard Feldman (Feb 10 2025 at 19:51):

another possibility: put end at the end of lambdas that don't end in a )

run_title_screen! : TitleScreenState => Model
run_title_screen! = |prev|
    state = { prev &
        rocci_idle_anim: update_animation(prev.frame_count, prev.rocci_idle_anim)
    }
    set_text_colors!()
    W4.text!("Rocci Bird!!!", { x: 32, y: 12 })
    W4.text!("Click to start!", { x: 24, y: 72 })
    draw_ground!(ground_sprite, 0)
    draw_plants!(plant_sprite_sheet, state.plants)

    shift = idle_shift(state.frame_count, state.rocci_idle_anim)
    draw_animation!(state.rocci_idle_anim, { x: player_x, y: player_start_y + shift })
    gamepad = W4.get_gamepad!(Player1)
    mouse = W4.get_mouse!()

    start = gamepad.button1 or gamepad.up or mouse.left

    if start then
        init_game(state)
    else
        TitleScreen(state)
    end
end

view this post on Zulip Anthony Bullard (Feb 10 2025 at 19:52):

Richard Feldman said:

that said, I definitely prefer not having to write def or let :sweat_smile:

we are planning on having var...

view this post on Zulip Richard Feldman (Feb 10 2025 at 19:52):

yeah but that's an edge case

view this post on Zulip Richard Feldman (Feb 10 2025 at 19:52):

for reference, this would be what it would look like to use parens instead of end in the lambda:

run_title_screen! : TitleScreenState => Model
run_title_screen! = |prev| (
    state = { prev &
        rocci_idle_anim: update_animation(prev.frame_count, prev.rocci_idle_anim)
    }
    set_text_colors!()
    W4.text!("Rocci Bird!!!", { x: 32, y: 12 })
    W4.text!("Click to start!", { x: 24, y: 72 })
    draw_ground!(ground_sprite, 0)
    draw_plants!(plant_sprite_sheet, state.plants)

    shift = idle_shift(state.frame_count, state.rocci_idle_anim)
    draw_animation!(state.rocci_idle_anim, { x: player_x, y: player_start_y + shift })
    gamepad = W4.get_gamepad!(Player1)
    mouse = W4.get_mouse!()

    start = gamepad.button1 or gamepad.up or mouse.left

    if start then
        init_game(state)
    else
        TitleScreen(state)
    end
)

view this post on Zulip Anthony Bullard (Feb 10 2025 at 19:53):

Richard Feldman said:

another possibility: put end at the end of lambdas that don't end in a )

run_title_screen! : TitleScreenState => Model
run_title_screen! = |prev|
    state = { prev &
        rocci_idle_anim: update_animation(prev.frame_count, prev.rocci_idle_anim)
    }
    set_text_colors!()
    W4.text!("Rocci Bird!!!", { x: 32, y: 12 })
    W4.text!("Click to start!", { x: 24, y: 72 })
    draw_ground!(ground_sprite, 0)
    draw_plants!(plant_sprite_sheet, state.plants)

    shift = idle_shift(state.frame_count, state.rocci_idle_anim)
    draw_animation!(state.rocci_idle_anim, { x: player_x, y: player_start_y + shift })
    gamepad = W4.get_gamepad!(Player1)
    mouse = W4.get_mouse!()

    start = gamepad.button1 or gamepad.up or mouse.left

    if start then
        init_game(state)
    else
        TitleScreen(state)
    end
end

I personally like this, but I know many other might not. It's a easier to manage mental model in terms of consistent usage of syntax. But for some it's just more "noise"

view this post on Zulip Anthony Bullard (Feb 10 2025 at 19:54):

I'm pretty vehemently anti-parens here

view this post on Zulip Luke Boswell (Feb 10 2025 at 19:55):

I have copied and pasted a lot of roc code... and found the significant whitespace really annoying. It's pretty minor, but every time I copy something I have to read back up to find the place where I inserted it and reselect it all again and then re-indent. Maybe I'm just driving the editor wrong... but if there is a way we can make it just always work and the formatter cleans things up, I feel like that is a significant improvement.

view this post on Zulip Brendan Hansknecht (Feb 10 2025 at 19:55):

Can someone ELI5 to me why end is better than ) or } (is it purely aesthetic or is it more diff)? Just feels like more noise. I feel like I must be missing something.

view this post on Zulip Anthony Bullard (Feb 10 2025 at 19:55):

|...| EXPR

and

|...| do
    STATEMENTS
    ...
    EXPR
end

would be a perfectly consistent approach IMO

view this post on Zulip Richard Feldman (Feb 10 2025 at 19:56):

Brendan Hansknecht said:

Can someone ELI5 to me why end is better than ) or }? Just feels like more noise. I feel like I must be missing something.

} is ambiguous with records, and ) on its own line looks funny :big_smile:

view this post on Zulip Brendan Hansknecht (Feb 10 2025 at 19:57):

and ) on its own line looks funny

hmm. I guess I am too used to any type of bracket on its own line

view this post on Zulip Brendan Hansknecht (Feb 10 2025 at 19:57):

So that feels irrelevant to me

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

I'm surprised that no(?) examples in this topic feature dangling parentheses from multi-line function calls, which I'm expecting to be very common already with the migration to PNC.

view this post on Zulip Richard Feldman (Feb 10 2025 at 19:57):

I don't like those either :big_smile:

view this post on Zulip Anthony Bullard (Feb 10 2025 at 19:57):

Also ')' here adds yet another bit of significance to that symbol

view this post on Zulip Anthony Bullard (Feb 10 2025 at 19:59):

Like is this going to be tuple item? or a function body?

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

this part of the conversation makes me want to revisit an idea from earlier:

the bodies of top-level functions must be indented some amount, with the exception of closing delimiters

view this post on Zulip Artur Domurad (Feb 10 2025 at 20:00):

I would definitely prefer end at the end of lambdas, than do before each "statement"

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:01):

the concern was:
Joshua Warner said:

Making the top-level a special case feels very weird to me
Either whitespace-based scoping should work everywhere or it should work nowhere.
I want to be able to copy-paste declarations from the top level to an inner level freely without having to change any extra things (besides indentation, obviously)

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:01):

Richard Feldman said:

this part of the conversation makes me want to revisit an idea from earlier:

the bodies of top-level functions must be indented some amount, with the exception of closing delimiters

The only thing I don't like about this is you have whitespace significance in the parser again, but probably the least troublesome sort

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:01):

yeah I just wonder if that's the least bad option

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:02):

I just always am going to argue for the most consistent option

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:02):

I get that, but we've gone down that path and hit a bunch of downsides

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:02):

so I want to re-explore the downside of that option

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:02):

Sure

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:03):

people don't seem to complain about spaces being significant around keywords, e.g. return needing to have a space after it

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:04):

so would it really be so bad if we said "each line of a top-level declaration needs to begin with some amount of whitespace, and we don't care how much or what the whitespace characters are?"

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:04):

to me personally, that feels like it's in a totally different category from significant indentation

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:04):

where I need to match things up

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:04):

there's nothing to match up in that design, it's just "these lines need some amount of space adjacent to them" just like return does

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:05):

I think I just don't know if this is really only a top-level problem

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:05):

I think its also a problem with a sequence of defs inside a def or lambda body

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:05):

hm, that's fair

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:06):

If it were I'd be up for it

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:06):

well, inside a when branch, the parser would keep going until it encountered either an end or another pattern for another branch, so I think it would be ok there

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:06):

same with if and either end or else

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:06):

inside a nested lambda it would be a problem though

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:08):

Yes or nested in a def. Which I think is little silly without shadowing anyway

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:08):

But we support it

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

another option we haven't discussed: use ; at the end of defs too, like Rust does:

run_title_screen! : TitleScreenState => Model
run_title_screen! = |prev|
    state = { prev &
        rocci_idle_anim: update_animation(prev.frame_count, prev.rocci_idle_anim)
    };
    set_text_colors!();
    W4.text!("Rocci Bird!!!", { x: 32, y: 12 });
    W4.text!("Click to start!", { x: 24, y: 72 });
    draw_ground!(ground_sprite, 0);
    draw_plants!(plant_sprite_sheet, state.plants);

    shift = idle_shift(state.frame_count, state.rocci_idle_anim);
    draw_animation!(state.rocci_idle_anim, { x: player_x, y: player_start_y + shift });
    gamepad = W4.get_gamepad!(Player1);
    mouse = W4.get_mouse!();

    start = gamepad.button1 or gamepad.up or mouse.left;

    if start then
        init_game(state)
    else
        TitleScreen(state)
    end

the argument for this would be that you just don't think about it, you always add it everyhere except the ending expression and you're all set

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

the argument against them would be that they aren't necessary

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:16):

and also that they're making the common case look noisier in order to accommodate the less-common case

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

I know I would just take an end

view this post on Zulip Brendan Hansknecht (Feb 10 2025 at 20:20):

So IIUC, we are basically saying that we could have a very minimal ruleset to deal with edge cases and roc could mostly stay the same while removing whitespace significance. The problem with this approach is that it feels inconsistent and so that pushes to trying to make a more visually consistent ruleset. Consistent rules tend to lead to syntax that are less nice than what roc currently has in one way or another.

This is kinda the loop being played through with various syntax?

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:25):

I think we are trying to find a syntax that

A) Removes a syntantic feature of a language that is very decisive for one reason or another (whitespace significance) for many (if not most) developers. Even if not most people that are current Roc users.
B) Does so while alienating the fewest current Roc users / changing the syntax the least

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:25):

yeah, I think the absolute minimum change to status quo (setting aside multiline strings, which can be a separate thing) which would make the parser no longer need to take indentation into account would be:

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:26):

I don't think there are any other cases that would be absolutely required to be different, unless I'm not thinking of something?

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:28):

end if required at the end of if/then/else as well?

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:28):

I don't think it's required for if/then/else

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:28):

as in, I don't think there's any ambiguity there

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:28):

so using it there would be more for consistency

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

So actually with optional else branches, there is ambiguity

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:29):

Right because you HAVE to have an else. It's block would always be the end (or a return)

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:29):

Joshua Warner said:

So actually with optional else branches, there is ambiguity

oh that's a good point - we do want to introduce those

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:29):

Optional else always ends in return, no?

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:29):

nah, could be a statement

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:29):

Oh shoot, yeah then you would want end for those

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:30):

So it's like, do you always want end, or only in the stead of else?

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:31):

to me, adding end to if and when feels like a totally fine change that I don't mind

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:31):

in isolation

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:31):

the problem is that I don't like {} = or _ = or do or ; nearly as much as status quo for statements :sweat_smile:

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:31):

Sure, then new multiline strings, and do is a separate convo

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:31):

Hehehehe

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:32):

oh yeah I guess also for and while need end, but again, that feels fine to me

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:32):

I think if I had to pick one of the alternatives for statements I'd lean towards do

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:32):

just because if I see semicolons in one place I expect them in other places (and I'm not a fan of them in general)

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:32):

Or just all blocks need an end delimiter, including lambda bodies

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:32):

and because _ = discards information and I like reading do more than I like reading {} =

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:33):

Anthony Bullard said:

Or just all blocks need an end delimiter, including lambda bodies

yeah, the thing is - I don't like this as much because it feels like making multiline lambdas - which are ultra common - less nice for the sake of statements, which are way less common

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:33):

I agree

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:33):

the reason I'm okay with end for if/when/for/while is that it doesn't feel like it makes them significantly less nice to me

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:33):

Just laying out the solution space

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:34):

unlike multiline lambdas, which do feel significantly less nice to me if they require end

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:34):

do seems like reasonable sugar

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:35):

And we don't have to be so opinionated in the formatter to force null defs into it

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:35):

unless we feel like that creates a rift in the ecosystem in terms of code style

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:36):

I do think in that world it might be nice to find a different keyword for loops

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:36):

like if do means {} = then it would be weird to also have it in for x in y do

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:36):

e.g. maybe for x in y loop might be better

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:36):

Maybe, but won't a for be mostly for statements?

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:36):

Or really exclusively?

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:37):

oh interesting

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:37):

yeah I guess arguably it is still being {} = :big_smile:

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:37):

that's kinda cool

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:37):

That's why do...end makes sense as a "no need for dos on statements in here"

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:38):

It's just a block where syntactic sugar is applied to all child statements that are null defs

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:38):

Direct children that is

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:40):

do EXPR is a "imperative statement" whereas do...end is an "imperative block" where imperative statements do not require a do modifier

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:43):

If I wasn't sick I'd design a nice 4 across spread of nicely highlighted code snippets showing the different alternatives

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:44):

but light-hearted Zulip banter is the best I can do at the moment :-)

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:46):

ok, so then I think the frontrunner concrete proposal based on all of this would be:

diff for roc-realworld

since that code base doesn't currently have any statements, here's how do would look in rocci bird:

run_title_screen! : TitleScreenState => Model
run_title_screen! = |prev|
    state = { prev &
        rocci_idle_anim: update_animation(prev.frame_count, prev.rocci_idle_anim)
    }
    do set_text_colors!()
    do W4.text!("Rocci Bird!!!", { x: 32, y: 12 })
    do W4.text!("Click to start!", { x: 24, y: 72 })
    do draw_ground!(ground_sprite, 0)
    do draw_plants!(plant_sprite_sheet, state.plants)

    shift = idle_shift(state.frame_count, state.rocci_idle_anim)
    do draw_animation!(state.rocci_idle_anim, { x: player_x, y: player_start_y + shift })
    gamepad = W4.get_gamepad!(Player1)
    mouse = W4.get_mouse!()

    start = gamepad.button1 or gamepad.up or mouse.left

    if start then
        init_game(state)
    else
        TitleScreen(state)
    end

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:48):

I think this needs a do:

    draw_animation!(state.rocci_idle_anim, { x: player_x, y: player_start_y + shift })

view this post on Zulip Richard Feldman (Feb 10 2025 at 20:49):

my overall thoughts on this compared to status quo:

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:49):

One thing I like about this is that the do keyword draws more attention to where the effects are occuring than the ! suffix

view this post on Zulip Niclas Ahden (Feb 10 2025 at 20:49):

Does anyone have some Roc HTML with intermingled when/if to share? I'm on the run with my phone so I can't easily pull anything up. It'd be useful to see HTML with change as I think it's a common use-case and when/if is often scattered within.

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:51):

@Niclas Ahden I think you have 90% of the world's Roc HTML code samples in client code ;-)

view this post on Zulip Anthony Bullard (Feb 10 2025 at 20:52):

Maybe 99%

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

yeah looking at roc-lang.org's main.roc it's 4 whens and 4 ifs across 231 lines of code, so wouldn't make much difference either way

view this post on Zulip jan kili (Feb 10 2025 at 20:57):

This sounds delightful!

view this post on Zulip jan kili (Feb 10 2025 at 20:59):

my = Hot.take |this!|
do this!
end

_ = my_concerns(previous_proposals)
for change! in latest_proposal do
    change!
end

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

we talked about that at the very beginning of the thread - much like with triple-quoted strings, I'd like to keep that as a separate discussion

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

it's hard enough to figure out just the indentation piece on its own :sweat_smile:

view this post on Zulip jan kili (Feb 10 2025 at 21:01):

Oops I'm not proposing anything, my joke just doesn't fully work haha - I meant to implement your proposal in joke form and failed by editing the first line :sweat_smile:

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:03):

Here's an example from roc-lang.org that is impacted(i think most of the impact in this file):

view = \page_path_str, html_content ->
    main_body =
        if page_path_str == "/index.html" then
            when Str.split_first(html_content, "<!-- THIS COMMENT WILL BE REPLACED BY THE LARGER EXAMPLE -->") is
                Ok({ before, after }) -> [text(before), InteractiveExample.view, text(after)]
                Err(NotFound) -> crash("Could not find the comment where the larger example on the homepage should have been inserted. Was it removed or edited?")
            end # ADDED
        else
            [text(html_content)]
        end # ADDED

    body_attrs =
        when page_path_str is
            "/index.html" -> [id("homepage-main")]
            "/tutorial.html" -> [id("tutorial-main"), class("article-layout")]
            _ ->
                if Str.starts_with(page_path_str, "/examples/") && page_path_str != "/examples/index.html" then
                    # Individual examples should render wider than articles.
                    # Otherwise the width is unreasonably low for the code blocks,
                    # and those pages don't tend to have big paragraphs anyway.
                    # Keep the article width on examples/index.html though,
                    # because otherwise when you're clicking through the top nav links,
                    # /examples has a surprisingly different width from the other links.
                    [id("example-main")]
                else
                    [class("article-layout")]
                end # ADDED
        end # ADDED

    page_info = get_page_info(page_path_str)

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:04):

(there's a when inside the first if that's missing an end)

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:06):

the very last end there is an example of one that I like

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:07):

as in, I prefer it to status quo

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:07):

because I always get a little nervous when there are big outdents

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:07):

like "is this actually at the correct indentation level to pick up where the previous thing left off, or did I outdent too far?"

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:07):

it's not a big thing, but in that particular case I like it

view this post on Zulip jan kili (Feb 10 2025 at 21:08):

Anyone here strongly against end if and end when and end for and end while?

view this post on Zulip Niclas Ahden (Feb 10 2025 at 21:08):

To beat a dead horse: this example shows what I fear. This is 4 LOC added in a small example. It's not the end of the world, but that is significant LOC growth throughout a project. PNC adds a few LOC as well in my experience, so overall we're getting less code on the screen.

view this post on Zulip Jonathan (Feb 10 2025 at 21:08):

Anthony Bullard said:

if page_path_str == "/index.html" then
            when Str.split_first(html_content, "<!-- THIS COMMENT WILL BE REPLACED BY THE LARGER EXAMPLE -->") is
                Ok({ before, after }) -> [text(before), InteractiveExample.view, text(after)]
                Err(NotFound) -> crash("Could not find the comm

This needs to have an end too, right? Just checking

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:09):

Niclas Ahden said:

To beat a dead horse: this example shows what I fear. This is 4 LOC added in a small example. It's not the end of the world, but that is significant LOC growth throughout a project. PNC adds a few LOC as well in my experience, so overall we're getting less code on the screen.

yeah, I totally agree this is a downside. :thumbs_up:

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

I do think PNC + static dispatch will end up being a net subtraction of LoC for what it's worth

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

I saw quite a few examples where today I'd write them in multiline |> with fully-qualified names, but in static dispatch they became comfortably single-line (due to not having the fully-qualified names anymore)

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

when doing roc-realworld and some others

view this post on Zulip Niclas Ahden (Feb 10 2025 at 21:11):

That may very well be, I think I just got scarred from PNC-ing some projects, esp. with HTML, and seeing an explosion. I took a note to try to design a new HTML DSL/API to make things better but haven't gotten started on that yet.

view this post on Zulip Niclas Ahden (Feb 10 2025 at 21:12):

Yeah, I'm really excited to try SD! It looks super clean :)

view this post on Zulip jan kili (Feb 10 2025 at 21:13):

For LOC trimming,

if long_fooooooooooooooooo then
    long_barrrrrrrrrrrrrrrr
else
    long_bazzzzzzzzzzzzzzzz
end

can always be

foo = long_fooooooooooooooooo
bar = long_barrrrrrrrrrrrrrrr
baz = long_bazzzzzzzzzzzzzzzz
if foo then bar else baz end

with no added LOC.

view this post on Zulip Niclas Ahden (Feb 10 2025 at 21:14):

And perhaps I'm just in state of hightened worry as changes have been happening and I'm so excited overall for this language. It's really impactful to me as I am trying to make it the thing that I write every day, and I evangelize quite a bit for it, so when it's changing I feel like "nooo, don't take it away from me!"

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:16):

I totally hear that!

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

naturally, I'm also trying to balance finding solutions for longstanding concerns :big_smile:

view this post on Zulip Brendan Hansknecht (Feb 10 2025 at 21:17):

The struggle of wanting to change things cause we are doing a full rewrite anyway and not wanting to change things cause we already have so many changes in the pipeline and it would be great to get those in the hands of users and real code for a bit before changing more.

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

yeah, also a great point

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:19):

@Brendan Hansknecht also I know you're not a fan of end in general - how strongly do you feel about that?

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:24):

@Niclas Ahden I think we could do those cases a favor and NOT outdent. Maybe we should only outdent where there is an explicit trailing comma in source (which we would retain after outdenting)

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:25):

It's a small bit to track in the parse ast

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:26):

And I would do this specifically only for Apply

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:26):

JanCVanB said:

For LOC trimming,

if long_fooooooooooooooooo then
    long_barrrrrrrrrrrrrrrr
else
    long_bazzzzzzzzzzzzzzzz
end

can always be

foo = long_fooooooooooooooooo
bar = long_barrrrrrrrrrrrrrrr
baz = long_bazzzzzzzzzzzzzzzz
if foo then bar else baz end

with no added LOC.

or we could simply:

if long_fooooooooooooooooo then
    long_barrrrrrrrrrrrrrrr
else
    long_bazzzzzzzzzzzzzzzz end

nailed it, ship it, no further iteration needed

view this post on Zulip Brendan Hansknecht (Feb 10 2025 at 21:26):

Not very. As I see it:

  1. Whitespace significance rarely bites me, so I don't mind it too much (though totally a pain when copying code)
  2. I'm not used to end and have only significantly used similar things in shell scripts like fi
  3. I find end more visually mixes into code than parens/braces in a way I find harder to parse/noisier.
  4. I feel like complex rules for when to apply end would likely lead to the most similar to current syntax, but that will feel really inconsistent.
  5. Parens/braces, I don't think would feel inconsistent if only applied when necessary.

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:26):

Yes, end doesn't have to be on a newline technically

view this post on Zulip Brendan Hansknecht (Feb 10 2025 at 21:26):

But I'm totally open to it being the right call

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:26):

fwiw I had never used end prior to Ruby and I got used to it pretty quickly

view this post on Zulip Brendan Hansknecht (Feb 10 2025 at 21:27):

Good to know

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:28):

I can say the same with Elixir (unless you count COBOL which I did for a year which has END * SECTION and more similar things I don't remember since it was 25+ years ago)

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:29):

we must attract the cobol programmers :100:

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:29):

that is the main motivation

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:29):

LOL

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:29):

Good use of the 100 emoji, since there are only 100 left

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:29):

They'll need a job once all that COBOL goes away - why not in Roc?

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:29):

I've actually heard that cobol programmers are in huge demand

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:30):

They get paid a lot

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:30):

Oh yeah, they do

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:30):

because there are a lot of companies (banks in particular) that have ancient load-bearing COBOL systems that they haven't been able to migrate off of, but also which they can't find anyone to work on anymore

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:30):

I knew that when I made the joke, but didn't want to let reality get in the way of comedy

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:30):

I probably would be financially independent now if I had - as my teacher suggested I do - dropped out of high school in 99 and became a COBOL programmer in California

view this post on Zulip Niclas Ahden (Feb 10 2025 at 21:32):

Anthony Bullard said:

Niclas Ahden I think we could do those cases a favor and NOT outdent. Maybe we should only outdent where there is an explicit trailing comma in source (which we would retain after outdenting)

I'm not following, could you post an example?

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:32):

Why has noone invented a language that compiles to COBOL?

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:34):

Niclas Ahden said:

Anthony Bullard said:

Niclas Ahden I think we could do those cases a favor and NOT outdent. Maybe we should only outdent where there is an explicit trailing comma in source (which we would retain after outdenting)

I'm not following, could you post an example?

Thinking about it, it won't really help Roc HTML since it takes two arrays

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:35):

@Joshua Warner @Luke Boswell curious what you think of this proposed design when you get a chance!

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:35):

(I think everyone else who's been active in the thread has weighed in?)

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

one thing just occurred to me: I remember a long time ago we had a beginner try to write the equivalent of this:

main! = |_args|
    Stdout.line!("hello,")
    Stdout.line!("world!")

and they were surprised that it didn't Just Work since if they deleted one of the lines it worked

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

in the current proposal, this would no longer Just Work

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

I wonder how much weight that should be given in regards to do vs other alternatives like end at the end of lambdas

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:39):

Not sure if it's a separate topic, but I think there may be benefit in desugaring do for the last statement like we used to do for !

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:39):

yeah I was thinking about that

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:39):

Would be nice to always write the same thing for statements

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:39):

a problem with that is that the formatter can't enforce it

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:40):

like it can't add a do

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:40):

because it doesn't have type info, so it doesn't know when it would be appropriate

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:40):

It kind of can

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:40):

It'd have to introspect for a ! at the top level

view this post on Zulip jan kili (Feb 10 2025 at 21:40):

I feel a void beneath us... (half-joking)

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:40):

Which would mean we can't do it if you incorrectly forget to put a ! in your effectful function name

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:40):

the ! function can still return a Str or something

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:40):

and it doesn't have to be annotated

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:41):

Then maybe it's just a warning

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:41):

I think a warning is fine

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:41):

I think this is a good argument for end at the end of lambda bodies. But I think I'm alone there

view this post on Zulip jan kili (Feb 10 2025 at 21:42):

I do worry that the farther we get from {} = the more surprising {} will be in the type system.

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:42):

Richard's point of lambdas being much more numerous than statements is the main thing

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:42):

I'd be really surprised by that

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:42):

You just need to hover Stdout.line! to see it's typed Str => {}

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:43):

If we end up in a world where we have {} = everywhere I wouldn't cry too much

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:43):

Anthony Bullard said:

I think this is a good argument for end at the end of lambda bodies. But I think I'm alone there

I think it's a good argument for them, I just still don't like them :laughing:

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:43):

maybe I'd get used to them though

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

another good argument for them is that it's probably the gentlest learning curve

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:44):

But I think that it would also mean the same on nested defs, like I said above

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:44):

Or we just disallow them

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:45):

because then the proposal is:

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:45):

and that's it

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:45):

I guess with the modification that lambdas inside parens don't need end (and I think it would probably be fine to generalize that to "you never need end when you have a ) anyway"

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:45):

because .map(|x| x + 1 end) feels totally unnecessary

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

but, to Niclas's point, now we're adding end in a lot more places

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:47):

I think frequency of use can trump consistency, it's why ? is better than try!(...) in Rust

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:48):

Anthony Bullard said:

Or we just disallow them

Do you agree that this is also required @Richard Feldman ? Any block that could contain statements (all of them) MUST be fully delimited

view this post on Zulip jan kili (Feb 10 2025 at 21:49):

Random note: It might be surprising to learners that if you add a return value to an effectful function you no longer do it.

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:50):

yeah, could be

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:50):

so

x =
    y = 1
    y + 2

would have to become

y = 1
x = y + 2

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:50):

yeah I think it's fine if nested defs need parens

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:50):

I think it'd be weird to say "you cannot do them in this language at all" but they're pretty rare anyway

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:51):

and on a personal level, I often find myself thinking "well technically it might be better to have a nested def here because strictly speaking the outer scope doesn't need to know about this, but also I kinda don't care"

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:51):

But them we have ambiguity with ()s. It not means "I delimit a block" and "I can wrap an expression"

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:51):

and having a little nudge to be like "don't worry about it, just put it at the same level, it's fine" kinda sounds appealing :laughing:

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:51):

Oh, and "I am a tuple"

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:52):

this would be an example of wrapping an expression

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:52):

Feels like do-end is better for that reason Anthony

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:52):

I like nested defs in languages where I can shadow, but not Roc

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:52):

I think the most general point about beginners is that I can see beginners being confused about when do (or for that matter _ = or {} =) is needed, but I can't imagine their being confused about end at the end of lambdas

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:53):

feels like the "end at end of lambda" design is strictly better for learning curve

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:54):

I just want to be clear:

x = (
    y = 1
    y + 2
)

Is not really wrapping an expression, that's wrapping a block

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:54):

Richard Feldman said:

feels like the "end at end of lambda" design is strictly better for learning curve

I agree with this

view this post on Zulip Sam Mohr (Feb 10 2025 at 21:54):

But this isn't record builders or effectfulness, it's just "big block has do"

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:54):

Anthony Bullard said:

I just want to be clear:

x = (
    y = 1
    y + 2
)

Is not really wrapping an expression, that's wrapping a block

I think this would have to be

x =
    y = 1
    y + 2
end

which looks awkward AF to me

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:55):

Anthony Bullard said:

I just want to be clear:

x = (
    y = 1
    y + 2
)

Is not really wrapping an expression, that's wrapping a block

actually today, this is an expression! :big_smile:

    y = 1
    y + 2

and I think it would continue to be in this design

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:56):

and you can already put parens around it today if you want

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:56):

Oh right, a Defs expr

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:56):

Which is not intuitive at all to those not familiar with the ML family

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:57):

I know that's really a let...in

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:57):

agreed, but again I think it's okay if people don't know about these because it's such an edge case

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:57):

like I don't think any beginners are thinking "oh no, how do I do this?"

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:57):

and if they are, they can always extract a function etc.

view this post on Zulip jan kili (Feb 10 2025 at 21:57):

If we add lambda-end (AKA "functiends"?), what other proposed syntax changes would we no longer need?

view this post on Zulip Luke Boswell (Feb 10 2025 at 21:57):

Sorry it's hard to keep up with the conversation happening so quickly.

Thoughts on https://roc.zulipchat.com/#narrow/channel/304641-ideas/topic/insignificant.20whitespace/near/498859198

I don't love the do on every line, and I think sometimes not having it when the effect returns something other than {} would be a little confusing.

I don't mind having the ends added, I think that feels ok.

I think I'm particularly interested in the do ... end block idea fencing off or segmenting an effectful place where things happen. This feels very similar to let .. in -- being where locals variables are defined.

I think I'm missing the connection between why we need do with every statement. Is it to delimit the end of the previous statement?

I also really like the idea of ending all statements with ;. Then the last expression or return value doesn't have an ;. Though maybe the issue there is that now we don't know the end of the final expression and the start of the next def.

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:57):

multiple ways to get unblocked even if you don't know it can be done

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:57):

So the real issue would be

x =
    y = 1
   doing_something!()
    y + 2

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:58):

right, that would need parens in this design

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:58):

JanCVanB said:

If we add lambda-end (AKA "functiends"?), what other proposed syntax changes would we no longer need?

do and do..end

view this post on Zulip Richard Feldman (Feb 10 2025 at 21:58):

@Luke Boswell thanks!

what do you think of the alternative design where we don't have do or ; but all lambdas need end at the end? (including top-level ones)

view this post on Zulip Anthony Bullard (Feb 10 2025 at 21:59):

Richard Feldman said:

right, that would need parens in this design

How is that different than statements elsewhere?

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

I don't think it is different :big_smile:

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

statements elsewhere also need either parens or an end

view this post on Zulip Anthony Bullard (Feb 10 2025 at 22:00):

:-)

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

I just don't think we should introduce do ... end for that case when parens are sufficient to solve the problem, and already work

view this post on Zulip Anthony Bullard (Feb 10 2025 at 22:00):

So you are saying that parens after lambda args is equivalent to ending the body with end?

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

yep!

view this post on Zulip Anthony Bullard (Feb 10 2025 at 22:01):

Ok

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

and I think the same could be true (and would seem reasonable to me) for surrounding if or when with parens instead of having end at the end for them

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

e.g. (if foo then x else y)

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

just stylistically I'd prefer end when there aren't already parens involved

view this post on Zulip Anthony Bullard (Feb 10 2025 at 22:02):

I just think it is incredibly weird to a mainstream programming of thinking of a sequence of lines like the above as anything but a block of statements at that point

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

I hear that, I just think it's so uncommonly used that it's not worth introducing special syntax for

view this post on Zulip Anthony Bullard (Feb 10 2025 at 22:03):

Like z = (y = x + 1 y) being an expression like z = (if foo then x else y) is just ... weird textually

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

like either we have to teach "in this one unusual case that you'll rarely if ever reach for, use this syntax that doesn't appear anywhere else" or else "in this one unusual case that you'll rarely if ever reach for, you might be surprised to learn that parens just work already"

view this post on Zulip Anthony Bullard (Feb 10 2025 at 22:04):

Ok, that sounds fair

view this post on Zulip Anthony Bullard (Feb 10 2025 at 22:04):

I guess I just never thought about how weird the Defs expr is with the way it's syntax looks

view this post on Zulip Anthony Bullard (Feb 10 2025 at 22:05):

But it's good syntax sugar, which languages like F# solves with whitespace significance

view this post on Zulip Anthony Bullard (Feb 10 2025 at 22:05):

(But you can use OCaml syntax too if you want - and not worry about indentation)

view this post on Zulip Anthony Bullard (Feb 10 2025 at 22:06):

Ok, going to rest my brain (and my hands)

view this post on Zulip Luke Boswell (Feb 10 2025 at 22:07):

Richard Feldman said:

Luke Boswell thanks!

what do you think of the alternative design where we don't have do or ; but all lambdas need end at the end? (including top-level ones)

Another idea... what if all lambdas require a return?

Does that also solve our problem?

view this post on Zulip Anthony Bullard (Feb 10 2025 at 22:07):

@Luke Boswell Don't you dare..... (Though it would be popular to some)

view this post on Zulip Luke Boswell (Feb 10 2025 at 22:08):

I like it. I think it's really beginner friendly

view this post on Zulip Anthony Bullard (Feb 10 2025 at 22:08):

(Also no, unless you want all blocks to have it) Actually maybe

view this post on Zulip Luke Boswell (Feb 10 2025 at 22:08):

Yeah, that's the way I'm leaning

view this post on Zulip Anthony Bullard (Feb 10 2025 at 22:09):

@JanCVanB May come for you :rofl:

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

hmmm

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

I definitely had not considered that, because I like having implicit return at the end of lambdas :big_smile:

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

but I think it would be an option

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

in the sense of like

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

"either you have a close paren or a return"

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

so .map(|x| x + 1) could still work

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

and then top-level functions would probably always use return

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

And then you have to remember to add return when you pull your lambda out to a def...

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

my knee-jerk reaction is that I don't like it, but maybe I should give it a chance

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

My knee is jerking really hard

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

objectively:

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

oh wait then you'd have to do return {} at the end of functions that don't return anything :joy:

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

I think that pretty much rules out that design

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

Unless you make void a thing...

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

not because of types, because you need a way to end the block

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

Yeah, I guess

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

otherwise the parser thinks the next top-level declaration is an inline block

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

so I think end at the end of lambdas now feels like the most beginner-friendly design

view this post on Zulip Luke Boswell (Feb 10 2025 at 22:28):

Richard Feldman said:

oh wait then you'd have to do return {} at the end of functions that don't return anything :joy:

What is a function that doesn't return anything?

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

anything with the type => {}

view this post on Zulip Luke Boswell (Feb 10 2025 at 22:29):

One of the things I've found an improvement with our latest syntax changes, is adding an explicit Ok({}) at the end of top level defs really clarifies what is happening, as opposed to just having Stdout.line!(...) which isn't immediately clear it's returning {}.

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

or perhaps more obviously, it would have to be:

main! = |_args|
    return Stdout.line!("Hello, World!")

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

@Niclas Ahden I'm curious what you think of this copium way of looking at things: elm-format puts two blank lines in between top-level declarations (like Python does), so having end at the end of top-level lambdas would space them out exactly as much as Elm does, while also not increasing the LoC of their bodies

view this post on Zulip Niclas Ahden (Feb 10 2025 at 22:33):

I agree that it's copium, and I would vote to change the formatter to one line in between :)

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

Elm's?

view this post on Zulip Niclas Ahden (Feb 10 2025 at 22:34):

Yeah

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

yeah, me too :joy:

view this post on Zulip Niclas Ahden (Feb 10 2025 at 22:34):

So for Roc I'd prefer the same: one line between functions (no end)

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

yeah same, all else being equal

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

what do you think about the end for lambdas design in the context of the other tradeoffs?

view this post on Zulip Niclas Ahden (Feb 10 2025 at 22:37):

RE do: I prefer ; over do because: do is three characters, {} = is five characters, while ; is one. Also, ; is at the end of the line, where other stuff like "what do I want to do with this return value" is, like ? and ??. However, I will need to shower after sending this message.

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

yeah, end at the end of lambdas would mean not needing do or ;

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

which I think would be easier for beginners to learn, plus experts wouldn't trip over needing to add do or ; in some places

view this post on Zulip Niclas Ahden (Feb 10 2025 at 22:41):

Richard Feldman said:

what do you think about the end for lambdas design in the context of the other tradeoffs?

I haven't been able to read all the latest messages so I can't say 100 %. I think it'd be a pity if we needed end for anything at all. The less the better. But I value consistency too, so it'd be weird to be able to skip it sometimes. I can't gauge how often I'd need to add end off-hand right now if we just needed them for when/if/lambda, but I feel like it's quite a few ends.

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

I'm avoiding asking how much we want this over sticking with WSS because I assume we'll do so once we agree what this would look like, all opinions considered

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

the "top-level body lines have to begin with a space or tab" option is still worth considering imo.

we talked about this being a downside:

Richard Feldman said:

inside a nested lambda it would be a problem though

but this would only come up for named lambdas that aren't top-level, which are pretty rare

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

might be fine to say those have to go in parens

view this post on Zulip Niclas Ahden (Feb 10 2025 at 22:46):

Overall I think the current situation is better than what's been described here. Probably because:

view this post on Zulip Luke Boswell (Feb 10 2025 at 22:52):

Richard Feldman said:

oh wait then you'd have to do return {} at the end of functions that don't return anything :joy:

Could return on its own be syntax sugar for either return {} or return Ok({})?

view this post on Zulip Niclas Ahden (Feb 10 2025 at 22:54):

Are there any other strong pros than these?

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

those are the ones I'm aware of :+1:

view this post on Zulip Niclas Ahden (Feb 10 2025 at 23:00):

Point #1 doesn't apply to me personally. #3 I disagree with (I think people feel strongly in both direction, see Elm/Haskell/Roc). I don't know how impactful #2 is. I could see this being a win if #2 is really strong. And, I've written a ton of Ruby and I didn't cry about it there. I did opt for { instead of do/end for all enumeration stuff, though.

view this post on Zulip Niclas Ahden (Feb 10 2025 at 23:01):

Anyway, time for that shower. I trust the process and you lovely people to make the best decisions as has been the case in all of Roc's life! Peace!

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

to put it out there as another concrete alternative to the status quo (again, setting aside triple quotes), here is a different possible design:

compared to today:

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

in terms of approachability and some people having a strong preference against significant indentation, I think there's a good chance that they would be okay with this.

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

when I hear the specific reasons people list when they say they dislike significant indentation, none of them apply to this design

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

I could see it downgrading someone from "I will not use this language" to "significant indentation fundamentally offends my sensibilities, but in this case I'll put up with it"

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

(I don't know this is how it would go down, but I don't know of any language which has tried this experiment)

view this post on Zulip Sam Mohr (Feb 10 2025 at 23:33):

Pretty minor, just wondering, do for and while still have do?

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

yeah

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

but it's just their version of is and then

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

nothing special

view this post on Zulip Sam Mohr (Feb 10 2025 at 23:34):

agreed

view this post on Zulip jan kili (Feb 10 2025 at 23:34):

I like it. For those hissers, the only moment when indentation would bite them is if they add a zero-indentation line mid-block, which is clearly avoidable and kind of silly to support.

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

honestly I think it would probably work to just not even mention it :stuck_out_tongue:

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

like just write the code and never mention that things have to be indented

view this post on Zulip Sam Mohr (Feb 10 2025 at 23:35):

The only lambda's I'm thinking of that are long are tail-call optimized loopers, but we have for and while now, so I don't think there's gonna be that much need for those

view this post on Zulip jan kili (Feb 10 2025 at 23:36):

We could even say "Roc has neither significant nor insignificant whitespace - it has automated whitespace" (despite the formatter's input domain being ws-sig)

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

because code editors indent for you automatically, so I feel like you'd almost have to go out of your way to write a top-level decl where the body is outdented

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

I literally think I would try not even bringing it up in the tutorial

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

and see what happens

view this post on Zulip jan kili (Feb 10 2025 at 23:37):

Can for and while have return values?

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

they shouldn't evaluate to expressions

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

like {} = for ... should always (conceptually) type-check

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

not saying that syntax should or shouldn't be valid

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

but I think it's a selling point for them to not evaluate to anything, because it makes it clearer when they are/aren't to be used

view this post on Zulip Sam Mohr (Feb 10 2025 at 23:38):

As with the ? expression-return vs. function-return discussions, you can always wrap for in a zero-arg lambda to make it return locally

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

a thing I dislike about for comprehensions is that they're approximately the same level of convenience as higher-order functions, but they're a separate syntax

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

whereas the convenience gap between for and a lot of tail-recursive functions and walk variants is much bigger

view this post on Zulip jan kili (Feb 10 2025 at 23:39):

A thing I like about your latest proposal here is that end is a keyword that matches with other keywords but not vertical bars.

view this post on Zulip Luke Boswell (Feb 10 2025 at 23:40):

Richard Feldman said:

to put it out there as another concrete alternative to the status quo (again, setting aside triple quotes), here is a different possible design:

compared to today:

Would you mind making an example with this syntax? Or is it literally just the same as current with the addition of ends?

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

yeah it's literally the same

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

except it's more permissive, like if you make your indents go all over the place the formatter just fixes them

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

as long as there's some indentation

view this post on Zulip Luke Boswell (Feb 10 2025 at 23:41):

That's nice.

I imagine it will also helpful if we are having the formatter use tabs everywhere for indentation.

view this post on Zulip jan kili (Feb 10 2025 at 23:42):

Mobile keyboards rejoice. 20+ year old terminal text editors rejoice.

view this post on Zulip jan kili (Feb 10 2025 at 23:46):

Just newline-space-coooode-newline-space-coooode-savefile.

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

I think this "any-indentation" design is my favorite of the ones we've discussed.

comparing it to the alternatives:

view this post on Zulip Luke Boswell (Feb 10 2025 at 23:48):

Could you explain this a little further?

view this post on Zulip Luke Boswell (Feb 10 2025 at 23:50):

# instead of this
top_level! = |arg|

    adder = |num|
        num + 2

    adder(2)

# we have this?
top_level! = |arg|

    adder = (|num|
        num + 2
    ) # the parens are mandatory?

    adder(2)

view this post on Zulip Luke Boswell (Feb 10 2025 at 23:52):

This is a really common pattern I've used, particularly to close over some args and make helpers.

view this post on Zulip Luke Boswell (Feb 10 2025 at 23:55):

Also are we losing the property that functions and lambdas are defined the same everywhere?

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

that's correct, that's how it would look

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

I don't think this affects the property...they're still lambdas, it's just sometimes you need parens to disambiguate

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

which is something that comes up in various different places, e.g. when using some operators together with one another

view this post on Zulip Sam Mohr (Feb 10 2025 at 23:58):

Is typing this a parsing error?

top_level! = |arg|
    adder = |num| num + 2

view this post on Zulip Luke Boswell (Feb 10 2025 at 23:58):

I'm not opposed to this, just wanted to clarify.

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

yeah, it would need to be:

top_level! = |arg|
    adder = (|num| num + 2)

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

this would also be unambiguous, although I'm not sure which would make more sense from a formatting perspective:

top_level! = |arg|
    adder = |num| (num + 2)

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

The former looks worse IMO, but is more consistent

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

In that it means when writing a lambda, you always need the whole thing to be contained in parens, and the second | isn't followed by a (

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

But the second one doesn't work the same as list.map(|arg| arg + 2)

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

The main thing is that ( after the second |

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

as with triple quotes, I think there are a variety of possible designs here and I'm not really worried about it :big_smile:

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

Well, I'm onboard

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

cool! I'm curious what @Joshua Warner and @Brendan Hansknecht think of this design

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

I'd like to congratulate @Anthony Bullard for his 2.5 month victory: #ideas > Explicit block end delimiters? @ 💬

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

that is, this design:

Richard Feldman said:

I think this "any-indentation" design is my favorite of the ones we've discussed.

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

Hmm, would type annotations mess with lambdas?

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

main! = |args|
    inspect = (|arg|
        bytes : List U8
        bytes = arg.bytes()
        bytes.inspect()
    )

    for arg in args do
        Stdout.line!(inspect(arg))
    end

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

Can we always distinguish when a type annotation ends and when the def starts?

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

I'm thinking of the (|x| when x is A -> 1 B -> 2)

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

If that is valid, then it feels like there could be some ambiguity with type annotations, though I can't think of a case yet that isn't "one space-delimited thing before = is the def, everything before that is the type annotation"

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:18):

if you specifically want to have a named lambda in an inline def (that is, named but not at the top-level), it needs to have parens around it.

This definitely feels strange. I think it won't come up that often, but it is nice to nest helper lambdas to the context they are used in.

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

sure, but it still works haha

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

it's not disallowed, it just doesn't look as nice as today

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:24):

I'm still not a fan of end, but I trust that I would get used to it. And if it leads to the benefits of never needing to worry about indentation and getting great auto formatting of even compressed single line code, sounds great

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:25):

it's not disallowed, it just doesn't look as nice as today

Yeah, just feels inconsistent/inconvenient. Pushes to figuring out a global name and making it a top level function for consistency

view this post on Zulip jan kili (Feb 11 2025 at 00:28):

@Richard Feldman How would you respond to someone genuinely asking "why not just braces-wrapped blocks?"

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

"is map(|x| { x }) returning a record or no?"

view this post on Zulip jan kili (Feb 11 2025 at 00:31):

Fair. What if "... multi-line* blocks?"

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

it's the same problem

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

y = {
    x
}

already means something

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

as Anthony noted, it's already a point of confusion in TypeScript even for advanced users

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:36):

And we don't want lambdas to end in end cause it gets too verbose.

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:36):

and is a lot less pleasing than the other control flow stuff

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:38):

Also, this proposal still technically has white space significance, but only for top level deps?

I think it will be an easy tripping point to copy a top level dep, paste it into another dep, and then tab it in.

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:38):

But, maybe our formatter could auto insert the parens (but that means our parser still has to parse significant whitespace, which kinda defeats the point of this)

view this post on Zulip David Mell (Feb 11 2025 at 00:38):

As someone whose first language was Lua, and likes Python's WSS. I think whitespace should either be significant everywhere, or all blocks should be ended with end. I strongly prefer a more consistent but more verbose syntax to mixing the two.

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

Brendan Hansknecht said:

I think it will be an easy tripping point to copy a top level dep, paste it into another dep, and then tab it in.

:raised: has that ever happened?

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

I've definitely copied inline lambdas out to the top level, but I'm not sure if I've ever gone the other way around :sweat_smile:

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

also, I'm actually not sure it's a bad thing to encourage top-level defs

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

certainly sometimes it is nice to capture args, but there's also definitely a benefit to being at the top-level because all the inputs to the function are explicit in the args

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

I'm not saying either is always better than the other, just that I've heard the case made (I think Evan was telling me he prefers it, if I remember right?) that top-level is the better default

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

so "supported but there's a nudge to do it the other way" doesn't feel like the worst thing to me

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:41):

has that ever happened?

I definitely have done it for snippets/short lambdas

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

what was the motivation for bringing it inline?

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:41):

But I also really like the nested helper lambda structure

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

do you think that would still be the case if we had for and while?

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:42):

That said, lambdaset bugs in roc made me mostly stop diong that

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

e.g. how much of that is specifically for recursion

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:42):

do you think that would still be the case if we had for and while?

Yeah, probably would mostly go away

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:43):

I think it is most common with some form of list function with a large enough helper to want it separate or with a recursive helper with base case

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

interestingly, the only places where I did this in roc-realworld wouldn't change their LoC

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

or at least, wouldn't need to

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

3 of the 4 are one-liners, and the 4th one already has a ) at the end due to a multiline function call, which could become ))

view this post on Zulip Jonathan (Feb 11 2025 at 00:45):

Brendan Hansknecht said:

do you think that would still be the case if we had for and while?

Yeah, probably would mostly go away

I'm curious - do you think that with the option of for and while that you would largely switch over? I've come to find it often more natural to express things with recursion, but I don't know if this is 'stockholm syndrome' :laughing:

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:46):

I feel like managing base cases almost always is easier with for/while loops.

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:46):

That said, some list pattern matching recursion is super natural

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 00:47):

Also, a lot of time, recursion with a nice api requires splitting out a helper function with a bunch of extra args. Those cases often map better to just having a while loop

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

yeah, I can appreciate the "language smallness" benefit of having a smaller set of primitives and not including looping constructs

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

but if both are there, I'm almost always reaching for the looping construct except in very specific use cases like traversing a recursive sum type or something

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

basically I only reach for recursion in cases where if I used a loop I'd build my own stack on the heap anyway

view this post on Zulip Jonathan (Feb 11 2025 at 00:55):

Digging for my own reasoning and came up with only 'it feels cool' haha :sweat_smile: Excited to try looping though

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

Brendan Hansknecht said:

That said, some list pattern matching recursion is super natural

This should be a separate topic, but I think these cases are solved by some while let equivalent:

while items_ is [first, ..rest] do
    print!(first)
    items_ = rest
end

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 01:22):

I don't think that is too valuable/important, that is just a weird for loop.

What it would be is more of

while bytes_ do
    when bytes_ is
        ['f', 'o', 'r', .. as rest] ->
            out_ = out_.append(For)
            bytes_ = rest
        ...
end

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

That's captured by my example syntax, but yes

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

I'm more thinking of pushing to and popping from a queue

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

Like for running breadth-first search

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 01:24):

how would your syntax deal with multiple patterns? I am talking about many patterns

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

Okay, yeah, wouldn't deal with it and would need a for loop

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

I don't think we should get into the specific in this thread, though

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

Brendan Hansknecht said:

if you specifically want to have a named lambda in an inline def (that is, named but not at the top-level), it needs to have parens around it.

This definitely feels strange. I think it won't come up that often, but it is nice to nest helper lambdas to the context they are used in.

I just realized that Luke's return suggestion from earlier could work here - e.g. the roc-realworld example could be:

handle_req! = |{ jwt_secret, log, db, now! }, req|
    auth! = |handle!| return to_resp(Auth.authenticate(req, now!()).and_then!(handle!))
    auth_optional! = |handle!| return to_resp(Auth.auth_optional(req, now!()).and_then!(handle!))
    from_json! = |handle!| return to_resp(req.body().decode(Json.utf8).and_then!(handle!)))
    from_json_auth! = |handle!|
        return to_resp(
            auth(req, now!())
            .and_then!(|user_id| handle!(user_id, req.body().decode(Json.utf8)?))
        )

basically adding return to them rather than adding parens around them

view this post on Zulip Luke Boswell (Feb 11 2025 at 01:52):

Yeah that's what I was thinking -- and then the extension of that was... "why not everything use return" ... and then I thought about that and I thought it's a little clearer what is happening.

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

yeah I was originally thinking about return in the context of all lambdas, but in the specific context of these uncommon lambdas I wouldn't mind it as much, and I could see an argument for preferring it over wrapping them in parens

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

oh, another thing I realized about this design: it still fixes the problem @Brendan Hansknecht mentioned earlier about pasting from other sources and having tabs and/or spaces in the indentation, and that messing things up - it would all Just Work no matter what was used for indentation, and the formatter would just fix it all immediately

view this post on Zulip Luke Boswell (Feb 11 2025 at 02:37):

So to clarify this is what it might look like? both of these are valid...

top_level! = |arg|

    add2 = (|num| num + 2)
    add3 = |num| return num.add(3)

    add2(1) + add3(1)

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

yep!

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

both are unambiguous

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

I personally prefer the return one just because it has fewer parens :big_smile:

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 02:47):

What happens for effectful lambdas that don't return anything

top_level! = |arg|
    fatal! = |inner_arg|
        Stdout.line!(inner_arg.to_dbg_str())
        Process.exit!(1)
        return

    fatal!(...)

view this post on Zulip Luke Boswell (Feb 11 2025 at 02:48):

don't return anything

In a future design we've talked about () being zero arg... maybe return is like a void return. Or maybe we have return {}

I suggested that in #ideas > insignificant whitespace @ 💬

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 02:49):

And for a lambda that is just a when ... is, would it be

top_level! = |arg|
    lambda = |list|
        return when list is
            [x, .. as rest] -> lambda(rest)
            _ -> ...
        end

view this post on Zulip Luke Boswell (Feb 11 2025 at 02:50):

The when would need an end wouldn't it?

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

also I think we could easily give nice error messages if you mess this up

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

because the symptom would be "it looks like you're doing a def of a named nested lambda and it's eating the entire rest of the top-level def's body...I bet I know what happened!"

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 02:55):

A bit strange for single expression lambda that are on one line or already wrapped in parens. Also strange that it isn't required for the top level, but seems kinda reasonable

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 02:56):

Could it be a lambda must be one expression or use return for the last expression? would that be unambiguous?

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

still ambiguous

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

can't tell if it's one expression or just a statement followed by more defs etc

view this post on Zulip Joshua Warner (Feb 11 2025 at 03:07):

Wow this is quite the thread

view this post on Zulip Joshua Warner (Feb 11 2025 at 03:08):

I don't quite see why parens are necessary in an inner closure?

view this post on Zulip Joshua Warner (Feb 11 2025 at 03:08):

Anyway, I like this :thumbs_up:

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

here's an idea for a tutorial snippet explaining the named inner closure thing:

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

You can define an inner named closure like this:

outer = |arg|
    inner = (|bar| baz(bar))
    stuff(etc)
    arg + 1

You might be wondering why there are parentheses there. This is because without them, you'll get a compiler error saying it's ambiguous whether the code after baz(bar) belongs to the outer function or to the inner function:

outer = |arg|
    inner = |bar| baz(bar)
    stuff(etc)
    arg + 1

The parentheses make it clear where the inner function ends. If you prefer, you can do this without parentheses by using an explicit return statement to end the inner function:

outer = |arg|
    inner = |bar| return baz(bar)
    stuff(etc)
    arg + 1

Since nothing can happen in a function after it has returned, it's unambiguous that the code after the return must belong to the outer function.

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

I would add a note about accidentally getting tabbing wrong and how it still just works

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:03):

But... wait. Isn't it not ambiguous, with the do syntax? I guess that got dropped?

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:03):

And without do, how is this not also ambiguous at the top level?

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

do or ; or {} = were _the_ things that got rid of that ambiguity, including at the top level

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

https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/insignificant.20whitespace/near/498889382

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

that's the current proposal

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:13):

Ahhh so the top level is different from nested layers

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:13):

I didn't read that carefully enough

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:15):

I feel like there must be a way to make that rule a bit more consistent

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

yeah, the alternative design would be to have the top-level and inner ones both need end, but that adds a ton of ends all over the place and also either still has an edge case to explain with map(|x| x + 1) or else that case needs an end too

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:16):

Also, I wouldn't call this insignificant-whitespace; it very much is still significant in this proposal (albeit less so)

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

sure

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

but copy/pasting just works in practice

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

add there are no indentation levels you need to get to agree with one another

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

True

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

so it feels like it doesn't really have any of the specific drawbacks of significant indentation that we've discussed :big_smile:

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

I think I'm kinda ambivalent as to indentation vs strict end/)/etc delimiters - but it feels pretty weird to me for the rules to be different at the top level vs nested levels

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

I'd _really_ like to have something that works uniformly

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

I would too, but all the other designs we've discussed have had worse drawbacks than this

view this post on Zulip Luke Boswell (Feb 11 2025 at 04:20):

Slight bump on my idea for having return in top-levels too... I think that's also an option (even if unpopular) :sweat_smile:

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

I guess I disagree with that assessment :shrug:

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

which specific design would you prefer?

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

any of the ones where the rules are consistent between the inner and nested levels :stuck_out_tongue_closed_eyes:

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

end for all lambdas does that, but I don't think anyone wants to have to write map(|x| x + 1 end)

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

Among the ones discussed tho, I think I'd most go for do notation, do/end as syntax sugar for parens, or just using straight parens (in that order)

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

I think if there's already a clear outer delimiter, we don't need you to put an inner delimiter

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

in that design, beginners have to learn this:

main! = |_args|
    do Stdout.line("hello, ")
    Stdout.line("hello, ")

and advanced users will be tripping over it all the time

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:24):

Oh true, that is weird

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

yeah I think we need to just rule out do and those variations

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:24):

Maybe bring back the requirement of a "true" last expression?

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:24):

e.g. in your case that could be {}

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:25):

I guess we couldn't really enforce that

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

yeah Luke suggested return {}

view this post on Zulip Luke Boswell (Feb 11 2025 at 04:25):

We could say it can't be effectful -- so Ok({}) or {} is permitted but not an Stdout.line!(...) -- but I don't think that solves the problem in general

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:25):

Is it a requirement that outer lambdas be delimiter-less (at the end)?

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

return definitely works just as well as end

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

it's not a requirement, but giving them delimiters has other downsides

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

Richard Feldman said:

end for all lambdas does that, but I don't think anyone wants to have to write map(|x| x + 1 end)

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

and then if we allow omitting end in there we're back in inconsistency land, plus also we've required adding a ton of ends all over the top level for no actual improvement in code clarity

view this post on Zulip Luke Boswell (Feb 11 2025 at 04:28):

Does return still work for a multiline expression? I think it may still be ambiguous

view this post on Zulip jan kili (Feb 11 2025 at 04:29):

I like mandating every function return. Now that we have early returns, the last line of a function isn't that special any more.

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

return is always a sufficient replacement for end because the function can't possibly have anything more to it after it returns

view this post on Zulip Luke Boswell (Feb 11 2025 at 04:29):

What about, throwback to my days writing Excel macros

main! = |_args|
    do Stdout.line("hello, ")
    Stdout.line("hello, ")
    end main!

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

but this is too weird imo:

main! = |_args|
    Stdout.line("hello, ")
    return Stdout.line("hello, ")

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

also, nobody wants to have to write map(|x| return x + 1) in a functional language :big_smile:

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

so I think we should rule out "return everywhere" too

view this post on Zulip Luke Boswell (Feb 11 2025 at 04:32):

I've got my creative hat on

view this post on Zulip jan kili (Feb 11 2025 at 04:32):

I'm still surprised that {} is becoming simultaneously the most common return type and the most hidden from app developers :laughing:

view this post on Zulip jan kili (Feb 11 2025 at 04:32):

Are we ashamed of our baby?! Does it need a makeover?

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

Richard Feldman said:

so I think we should rule out "return everywhere" too

I can stop holding my breath

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

parens could be used everywhere:

main! = (|_args|
    Stdout.line("hello, ")
    Stdout.line("world!")
)

but this to me feels like "let's make it look consistently unpleasant" :sweat_smile:

view this post on Zulip Luke Boswell (Feb 11 2025 at 04:36):

Lol, I just wrote a similar thing...

main! = |_args| (
    Stdout.line!("foo");
    Stdout.line!("bar");
)

run! = |_| (
    ...
)

view this post on Zulip jan kili (Feb 11 2025 at 04:36):

Ooh if we're getting creative/funky with it, here's a probably bad idea: what about bar-wrapping the |return_value| too?

view this post on Zulip Luke Boswell (Feb 11 2025 at 04:37):

Interesting...

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:37):

What if there are two lambda syntaxes - func(|x| x + 1) for one-line lambdas, and:

func = |x| do
    Stdout.line!("hello")
    x + 1
end

for multi-line ones?

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:37):

I guess more specifically, |x| x + 1 isn't so much "multi-line" as it is "single expression"

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:38):

i.e. no value/type defs allowed in that syntax; just a single expression

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

hm, interesting

view this post on Zulip Luke Boswell (Feb 11 2025 at 04:39):

main! = |_args| do
    adder = (|n| "n + ${Num.to_str(n)}")
    Stdout.line!(adder(1));
    Stdout.line!(adder(2));
end

run! = |_| do
    ...
end

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

so that implies do ... end is not specific to lambdas

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

it's just another way to write ( ... )

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

right?

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:40):

Why does it have to be? I definitely see why you'd want to extend that, but we can also just be like, "yo that's just the way it is"

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:40):

Parens would work equally well in that context, yes

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

I still like the value of having there be only one syntax for lambdas

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:40):

So in that sense they are replaceable

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

plus I think it's sort of implied to be supported haha

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:41):

If do/end is just sugar for parens, then there kinda is only one lambda syntax

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

exactly!

view this post on Zulip Joshua Warner (Feb 11 2025 at 04:41):

Just sometimes you need to "use" it differently, depending on the surrounding context

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

like if |x| expr is valid and |x| do ... end is valid, then it certainly looks like do ... end is an expr :big_smile:

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

that also means it can be used for nested defs without parens, which came up earlier

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

What are the downsides to that approach?

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

I'm sure this is not the design Niclas would prefer, due to it adding a bunch of do and ends to code bases

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

it is more consistent, that's for sure

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

although one-liner lambdas don't change

view this post on Zulip jan kili (Feb 11 2025 at 04:45):

JanCVanB said:

Ooh if we're getting creative/funky with it, here's a probably bad idea: what about bar-wrapping the |return_value| too?

Just to see how it looks:

y = |x| |x + 1|

main! = |_args|
    adder = |n| |"n + ${Num.to_str(n)}"|
    Stdout.line!(adder(1));
    Stdout.line!(adder(2));
    ||

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

it looks super ambiguous to me :sweat_smile:

view this post on Zulip jan kili (Feb 11 2025 at 04:48):

I like it more than I expected - 10/10 in symmetry

view this post on Zulip jan kili (Feb 11 2025 at 04:49):

2/10 in weirdness budget, though very obvious how it works

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

a minor downside to do ... end would be that you need to remember to add it when going from one expr to multiple.

I saw a beginner try this in Roc once, and they were surprised that it didn't work (at the time)

main! = |_args|
    Stdout.line!("hello, ")
    Stdout.line!("world!")

but we could give a great error message for this I think

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

that said, we could have a formatting convention of always using do ... end if it's multiline, even if it's not strictly necessary

main! = |_args| do
    Stdout.line!("hello, world!")
end

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

We could also sneakily make the parser whitespace-sensitive and have it "do the right thing" but give an error about ambiguous syntax (which then gets fixed on formatting by surrounding with do/end.

view this post on Zulip Luke Boswell (Feb 11 2025 at 04:52):

Joshua Warner said:

Maybe bring back the requirement of a "true" last expression?

Could we revisit this... I think there is something here worth thinking about.

Could we say the the lambda has to end with the exact type -- i.e. it's not an expression but a literal (tags with payload are also acceptable)?

main! _ => Result {} [Error]
main! = |_args|
    adder = (|n| "n + ${Num.to_str(n)}")
    Stdout.line!(adder(1))
    Stdout.line!(adder(2))
    Ok({})

run! = |_|
    ...

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

I like this more than just requiring end at the end of lambdas because:

view this post on Zulip Luke Boswell (Feb 11 2025 at 04:53):

Ironically as the guy who's suggested return for all lambdas... I'm not loving the do end for all lambdas. :sweat_smile:

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

Luke Boswell said:

Could we say the the lambda has to end with the exact type -- i.e. it's not an expression but a literal (tags with payload are also acceptable)?

I don't think you can depend on the type. Has to be parser level info

view this post on Zulip Luke Boswell (Feb 11 2025 at 04:54):

Ahk that makes sense.

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

here's a strange thing I just discovered about myself:

map(|x| x + 1)
main! = |_args| do
    Stdout.line!("hello, world!")
end
main! = |_args|
    Stdout.line!("hello, world!")
end

the first two look normal and natural to me, and the last one looks strange

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

I think it's maybe because I expect end to be paired with an opening keyword and not a symbol?

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

I don't see the difference between the last two

view this post on Zulip Luke Boswell (Feb 11 2025 at 04:56):

So @Joshua Warner -- have we twisted your arm yet? have you come around to compromising on the top-levels have to be the same as everything else? or could you accept that it is whitespace significant (but only a little bit significant in this one special case).

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

Oh "do"

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

I guess it's pretty unobtrusive! :laughing:

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

do-end is way better than parens

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

Honestly if I had to pick one thing, it'd probably be the thing we do currently - i.e. whitespace sensitivity.

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

Same

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

do/end would be a close second

view this post on Zulip Luke Boswell (Feb 11 2025 at 04:59):

Another idea... I don't love it

main! = |_args|
    Stdout.line!("hello, world!")
;

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

is there any possible way we could get the current indentation sensitivity to work with pasting?

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

or is there any variation on it we could do to improve that situation?

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:03):

Is it the mixed spaces/tabs thing? Or is this where you paste something that's indented too little into a block?

view this post on Zulip jan kili (Feb 11 2025 at 05:03):

I'd love to see examples of these paste cases people are tripping on, cause I might be imagining something different.

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

Joshua Warner said:

Is it the mixed spaces/tabs thing? Or is this where you paste something that's indented too little into a block?

both :big_smile:

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:03):

mixed spaces / tabs is solvable I think

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:04):

Will need to give some thought to the latter

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

I think the problem there that I usually run into is copying over a chunk of defs followed by a conditional

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

and the source was at a different indentation level than the destination

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

so I have to paste it in and then adjust the indentation level manually until I get it right

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:07):

Why is the "followed by a conditional" relevant/important?

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

it might not be, just a common part of the pattern for me

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

although if it's a when and I'm pasting into another when branch, I don't think that's solvable with the current design

view this post on Zulip Luke Boswell (Feb 11 2025 at 05:09):

I'm just saying... if we want to be a functional language in imperative clothing we should embrace the return :smiley:

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

we ruled that one out though :big_smile:

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:09):

Yeah there are definitely cases where you've lost too much info to recover, at least if you only look at the tokens blindly.

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

I wonder about like...what if indentation was just for lambdas?

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

and we added end to if and when

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

I guess that still wouldn't work

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

That definitely constrains the problem a lot

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

because what if you paste an outdented thing into a lambda

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

Sure, but now you solved the pasting-into-when case

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

true

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

oh wait

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

yeah that's actually potentially great

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

because it would mean pasting would likely only break when pasting into a nested lambda

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

which would be super rare

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:13):

We could have the formatter put parens around nested lambdas

view this post on Zulip Luke Boswell (Feb 11 2025 at 05:13):

Almost always I'm pasting at a top-level when I find it doesn't work. So if this design is tolerant for that then awesome.

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

handle_req! = |{ jwt_secret, log, db, now! }, req|
    auth! = |handle!| to_resp(Auth.authenticate(req, now!()).and_then!(handle!))
    auth_optional! = |handle!| to_resp(Auth.auth_optional(req, now!()).and_then!(handle!))
    from_json! = |handle!| to_resp(req.body().decode(Json.utf8).and_then!(handle!)))
    from_json_auth! = |handle!|
        to_resp(
            auth(req, now!())
            .and_then!(|user_id| handle!(user_id, req.body().decode(Json.utf8)?))
        )

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

so in that design, nested named lambdas look and work the same as today

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

and don't need any special explanation

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

and unless they're named, the nested lambdas will have parens or commas delimiting them anyway due to PNC

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:16):

@Luke Boswell you mean you've pasted in something over-indented and it accidentally gets slurped into the previous def?

view this post on Zulip Luke Boswell (Feb 11 2025 at 05:16):

I have my cursor at the start of a line... I copy something in and it gets indented... so I need to go back and re-select everything and un-indent it all back to the top-level

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

What about double newlines \n\n between top-level defs?

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

So vertical whitespace significance instead of horizontal?

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

What if (hear me out!), we have a sneaky zero width separator unicode character that we treat as a lambda delimiter in the parser, and we have the formatter inject that

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

(I'm only half joking)

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

Explain yourself

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

Or perish

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

This would function very similar to the end in |x| x + 1 end, except it's (1) invisible, (2) optional, and (3) we never expect users to enter it - only the formatter.

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

The (huge!) downside is that could lead to accidentally screwing up your file in a way that takes a hex editor to figure out and fix

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

The part of me that knows who Richard Stallman is thinks that requiring the formatter to make the code "optimal" isn't a good idea

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

A really interesting idea, though...

view this post on Zulip Luke Boswell (Feb 11 2025 at 05:23):

Luke Boswell said:

What about double newlines \n\n between top-level defs?

I've had feedback in the past that some people consider it bad practice to have too much whitespace within a lambda. So this would make that a convention automatically - and solve our pasting problem.

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

That's a form of whitespace significance that should be obvious enough to readers to not be as much of an annoyance

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

Just mash enter until compile good

view this post on Zulip Luke Boswell (Feb 11 2025 at 05:24):

It feels natural to me. Like how you separate a paragraph in an essay.

view this post on Zulip Luke Boswell (Feb 11 2025 at 05:25):

Each top-level def is like a new paragraph

view this post on Zulip Luke Boswell (Feb 11 2025 at 05:25):

So you could have at most one empty line inside a block.

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:26):

@Luke Boswell are you copying that thing from a scope that was indented more? Or is something (editor/language server/etc) accidentally causing the pasted lines to get an extra indent?

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

for the sake of thoroughness, I think it's worth thinking about do .. end except with curly braces, because I think it doesn't have the downsides we've discussed before:

map(|x| x + 1)
main! = |_args| {
    Stdout.line!("hello, world!")
}

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:26):

take my money

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

this is more concise than do .. end, and { x } can unambiguously mean what it does today: a record

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:27):

I guess that does mean you're not allowed to (directly) return a record

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

because if the whole point of the braces is to take the place of multiple statements, why would you ever try to use them as { x }?

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:29):

I don't quite follow that part

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

my claim is that if you take all the valid record expressions today

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

we can say those still mean what they do today

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

because you'd never want to use curlies for multiple expressions in a way that could be mistaken for a record

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

this is not a valid record:

main! = |_args| {
    Stdout.line!("hello, world!")
}

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

and nobody would write this:

main! = |_args| {
    x
}

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

So commas are the difference

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

and if they wrote this, I think it would obviously be a record:

main! = |_args| { x }

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

How would if/when/for look in this world?

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

so basically, curly braces can be either blocks or records depending on what you put in them, but I think it's the case that what you want to put in one doesn't overlap with what you'd want to put in the other, and vice versa

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

Sam Mohr said:

How would if/when/for look in this world?

could be the same as today, or could change

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

e.g. could just do what Rust does if desired

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

this is not a valid record:

main! = |_args| {
    if foo {
        Stdout.line!("hello, world!")
    } else {
        other
    }
}

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

but it sure does look C-like :joy:

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

I'm glad Anthony's asleep

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

It's very consistent

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:36):

I don't like the record/block confusion

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:37):

That is one thing that parens have going for them - we've defined it so there's no such thing as a one-element tuple, and so this can never be an issue

view this post on Zulip jan kili (Feb 11 2025 at 05:37):

Richard Feldman said:

to me, changing to braces is what feels like the "bridge too far" to me :stuck_out_tongue:

over the river and through the woods, to grandmother's langs we gooo :stuck_out_tongue_closed_eyes:

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

like I said, "for the sake of thoroughness" haha

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

I will say, I feel pretty torn between these two designs now:

main! = |_args|
    Stdout.line!("hello, world!")
main! = |_args| do
    Stdout.line!("hello, world!")
end

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

obvious the former is more concise, but the latter has a lot of nice properties and simplifications

view this post on Zulip Elias Mulhall (Feb 11 2025 at 05:41):

Dear Lord y'all, this is a long one!

Fwiw in JavaScript foo => ({ bar }) is how you return a record

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

I'm not sure how bad it is to have end after every single block except for top-level defs

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

Because that's a consistent rule, and would help remove a lot of these

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:41):

@Sam Mohr why would top level defs be excluded there?

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

To make your code, that's it

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:42):

Or oh you're suggesting an alternative

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

yes

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

One that was mentioned prior

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:42):

That's not consistent with _depth_

view this post on Zulip Joshua Warner (Feb 11 2025 at 05:42):

i.e. the top level is different from nested levels

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

Yes

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

That's what I meant by inconsistent

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

I don't like that part

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

I'm using the word differently than you, and yes, I understand

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

I think using do ... end for multiline blocks makes a lot of sense

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

that one feels like the simplest, most consistent design that has the desired properties

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

it's less concise than status quo

view this post on Zulip jan kili (Feb 11 2025 at 05:45):

If it's only for multiline blocks, does that reopen the door for a multiline-only return?

main! = |_args|
    Stdout.line!("hello, world!")
    return

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

right now time I'm imagining all the world's Elixir and Ruby programmers rolling their eyes at the same time :joy:

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

there is no return door

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

it has passed the point of no return

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

more

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

future return suggestions will be marked return to sender

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

I don't know how more of these I've got :sweat_smile:

view this post on Zulip Luke Boswell (Feb 11 2025 at 05:47):

I'm sure it will return later... once we've exhausted all other options...

view this post on Zulip Luke Boswell (Feb 11 2025 at 05:48):

It will return in your syntax dreams

view this post on Zulip Brendan Hansknecht (Feb 11 2025 at 05:48):

Richard Feldman said:

I will say, I feel pretty torn between these two designs now:

main! = |_args|
    Stdout.line!("hello, world!")

main! = |_args| do
    Stdout.line!("hello, world!")
end

Just to clarify, the first here is just status quo roc today and the second is alway having an explicit end for anything multiline to remove WSS?

view this post on Zulip jan kili (Feb 11 2025 at 05:49):

Luke Boswell said:

It will return in your syntax dreams

courage-the-522399599.gif

returrrn the slab block

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

first could be either status quo, or a variation where we treat either only lambdas or only top-level declarations as having significant indentation

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

the second one is the idea to have do ... end be equivalent to ( ... )

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

Jan I love you so much for that reference

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

and then say that lambda bodies are always 1 expression

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

but the formatter would choose to add do ... end if you make that one expression multiline

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

as opposed to map(|x| x + 1)

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

where it wouldn't add anything to the 1 expression

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

in that design, named nested lambdas work the same way as top-level ones

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

there's no indentation sensitivity anywhere

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

it feels like the simplest and most consistent design, but it has the drawback of being less concise

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

which I suppose is the general drawback of anything compared to significant indentation, to be fair :big_smile:

view this post on Zulip jan kili (Feb 11 2025 at 05:56):

Conciseness is sometimes anti-correlated with mental load for human readers!

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

true, but I don't think so in this case

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

I don't think either of these is at all hard to parse

main! = |_args|
    Stdout.line!("hello, world!")
main! = |_args| do
    Stdout.line!("hello, world!")
end

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

the second one is more verbose, although I have to say, the more I look at it, the more I kinda like the shape of it

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

I dunno why, just aesthetically

view this post on Zulip jan kili (Feb 11 2025 at 06:02):

One thing these examples don't show is how in a lot of files, the left side of top-level defs would look like (for better or worse)

    ...
    ...
    ...
end

foo = |...| do
    ...
    ...
    ...
    ...
    ...
    ...
    ...
    ...
    ...
    ...
    ...
    ...
end

bar = |...| do
    ...
    ...
    ...

(trying to highlight the end\n\ndef pattern)
(this is unrelated to my previous message)

view this post on Zulip jan kili (Feb 11 2025 at 06:07):

Eleventh hour random idea: done instead of end :smiley:

view this post on Zulip jan kili (Feb 11 2025 at 06:16):

main! = |args| do
    inspect = |arg| do
        bytes : List U8
        bytes = arg.bytes()
        bytes.inspect()
    done

    for arg in args do
        Stdout.line!(inspect(arg))
    done
done

merge_chunks_into_final_file! = |...| do
    info!(job_id, "Merging chunks for ...")

    if verify_file_size!(final_file_path, expected_file_size) then
        info!(job_id, "Chunks of ... has already been merged into ...")
        return Ok(final_file_path)
    done

    verify_total_size_of_chunks!(...)?

    when timed!(merge_chunks!(...)) is
        Ok((time_taken, final_file_path)) ->
            info!(job_id, "Merging chunks into \"${final_file_path}\" took ${time_taken}.")
            Ok(final_file_path)
        Err(e) ->
            info!(job_id, "Cleaning up failed merge ...")

            when delete_file!(...) is
                Ok(_) ->
                    info!(job_id, "Deleted output of failed merge ...")
                Err(e) ->
                    # It's OK if this file isn't successfully removed. Either it doesn't exist, or it'll be
                    # cleaned up later by our cron jobs anyway. This _could_ result in files lingering and eating
                    # up all disk space, but that's an acceptable risk (low probability, low impact, and easily
                    # detectable).
                    warn!(job_id, "Failed to delete output of failed merge ...")
            done

            Err(...)
    done

    # ... verify the file size of the merged file

done

view this post on Zulip jan kili (Feb 11 2025 at 06:20):

Serious proposal - many would enjoy the symmetry.

view this post on Zulip jan kili (Feb 11 2025 at 06:21):

Disclaimer: I've never used a lang with end, so this simply reads smoother to me.

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

Very bash

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

going back to the "indentation sensitive, but consistent and as error-tolerant as possible" design, what about this?

this could be taught as "you have to indent lambda bodies" but it could be very tolerant to pasting problems because as long as you're inside any sort of delimited area (such as a when branch), there's no significant indentation

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

so tab space tab is just 3 characters

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

And tab isn't "longer" than space?

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

I was thinking more that like if you have this:

    foo = |arg|

then if you want to put a statement or def in its body, then that statement or def has to have the exact same whitespace sequence that precedes foo = plus at least 1 more whitespace char of your choice

view this post on Zulip Luke Boswell (Feb 11 2025 at 06:27):

Based on the extensive discussion, here are my thoughts on the best design for making Roc indentation-insensitive:

Best Option: do...end blocks with consistent rules

Key aspects:

  1. Use do...end for multiline blocks consistently:
main! = |args| do
    doStuff!()
    moreStuff!()
end
  1. Single-expression lambdas remain concise:
list.map(|x| x + 1)
  1. Control structures (if/when/for/while) also use do...end:
if condition do
    stuff()
end

when value is
    Ok(x) -> do
        handleSuccess(x)
        cleanup()
    end
    Err(e) -> handleError(e)
end

Advantages:

  1. Fully consistent rules - blocks are delimited the same way everywhere

  2. No special cases for top-level vs nested declarations

  3. Clear visual structure with matching delimiters

  4. No ambiguity with records unlike using curly braces

  5. More familiar to developers coming from Ruby/Elixir

  6. Copy-paste works reliably since indentation doesn't matter

  7. Parser becomes simpler without whitespace significance

  8. Good error messages possible since block boundaries are explicit

  9. Formatter can enforce consistent style while parser remains flexible

Trade-offs:

  1. More verbose than current whitespace-sensitive syntax

  2. Requires learning one more keyword pair (do/end)

While this adds some verbosity compared to significant whitespace, it provides:

The increased verbosity seems worth it for these benefits, especially since the single-expression case remains concise.

Alternative approaches like requiring return or using braces each have more serious downsides around ambiguity or readability.

This design balances multiple concerns while keeping the rules simple and consistent. The fact that it aligns with established patterns in Ruby and Elixir suggests it's a reasonable approach that has proven workable in practice.

view this post on Zulip Luke Boswell (Feb 11 2025 at 06:27):

Lol... someone had to try it

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

well good to know what Claude (?) thinks :laughing:

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

Richard Feldman said:

I was thinking more that like if you have this:

    foo = |arg|

then if you want to put a statement or def in its body, then that statement or def has to have the exact same whitespace sequence that precedes foo = plus at least 1 more whitespace char of your choice

importantly, in this design, only the beginnings of defs and such inside the lambda need to have any indentation at all

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

so if I have a conditional inside there, and I paste into one of its branches, I'm all set unless maybe I'm pasting a nested named lambda or something

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

this design is almost equivalent to the "only top-level declarations have to be indented" design, except that nested named lambdas use significant indentation instead of needing separate rules

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

which means pasting always just works except in the very specific (and rare) case of pasting into a nested named lambda, in which case there's a chance it might not work

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

but given how rare those are, it would seem pretty unlikely for anyone to run into it in practice

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

I think the reason I keep coming back to this is that typing out all those dos and ends, and then reading them, just doesn't feel like it pays for itself

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

like yeah it's the simplest design, but the practical upside of that simplicity feels very small

view this post on Zulip jan kili (Feb 11 2025 at 06:42):

Given that the delta between that and the other current frontrunner design seems to just be functiend, would it make sense to weigh Zig compiler dev sequencing in this decision? Like would this delta be more sensible as an MVP or to revisit as a later quality of life improvement?

view this post on Zulip jan kili (Feb 11 2025 at 06:43):

I've read this whole topic as "what should the syntax change to in six months" btw

view this post on Zulip jan kili (Feb 11 2025 at 06:48):

If completely eliminating whitespace significance means the compiler is smaller or finishes sooner, postponing WSS improvements seems worth it, unless it would require a restructuring.

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

Since we're working in parallel and Anthony and Josh know what they're doing, I really don't think that this would change our completion rate

view this post on Zulip jan kili (Feb 11 2025 at 06:56):

This "you have to indent lambda bodies" compromise alternative feels good. Just good. An incremental improvement from today, with no new spicy bits, and with a few pasting problems solved.

view this post on Zulip Jasper Woudenberg (Feb 11 2025 at 07:49):

For the pasting code problem specifically, could that be solved by the editor? I know for instance that Android studio automatically reindents code when you paste it in.

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

That's only possible because Android Studio understands the intended syntax even with incorrect formatting

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

Mainly because Java has unambiguous syntax with respect to newlines

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

Current Roc finds it easy to break the syntax such that the code now doesn't know who owns which statements

view this post on Zulip Jasper Woudenberg (Feb 11 2025 at 07:55):

If the cursor position were taken into account when pasting, that might make something possible?

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

That would depend on the editor

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

The compiler doesn't get that info when formatting

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

Unless we did some diff magic with the LSP?

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

Pretty sure that's not a thing

view this post on Zulip Jasper Woudenberg (Feb 11 2025 at 07:58):

For me, the biggest downside of delimiters is still that it introduces a new type of error: Parsing fails because a missing end. With delimited languages I run into this one every now and then, and have no alternative but to manually comb through a potentially large body of code to find where I messed up, which is pretty unpleasant. So in terms of error tolerance of the parser, it's a net negative for me.

view this post on Zulip Anthony Bullard (Feb 11 2025 at 10:50):

I don't really understand what the state of this discussion is :cry:

view this post on Zulip Niclas Ahden (Feb 11 2025 at 11:20):

"Exploratory creative thinking", with all of the plausible combinations on the table, it seems :sweat_smile: It's actually a really good discussion, though, and I'm glad things are dealt with this way! I don't have the context window to keep it all in my head, sadly.

view this post on Zulip Niclas Ahden (Feb 11 2025 at 11:21):

I've tried to read everything I've missed and I'll respond below:

RE: Copy/paste
The copy/paste problem is, to me, just "how it works". You have to ensure it lines up when you paste, just like you'd have to make sure to include the correct curly braces etc. when you copy in another language. You can't escape this.

What's worse:
A) Copy/paste alignment
B) Having to type and read do ... end ... do ... end ... do ... end; when forgetting an end, you get similar symptoms as misaligned copy/paste (the formatter goes nuts or you get weird compiler errors); and LOC growth

I'd prefer A, as that's not as common, and at least I'm very aware that I just pasted something and that's why I'm getting an error (if I forgot to align). Forgetting an end can happen any time (including when copy/pasting).

Outside of copy/paste, i.e. when typing code, I find that forgetting an end is more common than making an alignment mistake, as editors are good at alignment.

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

I would guess this is the front runner... https://roc.zulipchat.com/#narrow/channel/304641-ideas/topic/insignificant.20whitespace/near/498936924

view this post on Zulip Niclas Ahden (Feb 11 2025 at 11:23):

Out of the alternatives posted, I think this seems like a decent compromise and least harmful:

Richard Feldman said:

going back to the "indentation sensitive, but consistent and as error-tolerant as possible" design, what about this?

this could be taught as "you have to indent lambda bodies" but it could be very tolerant to pasting problems because as long as you're inside any sort of delimited area (such as a when branch), there's no significant indentation

I'd still prefer the status quo, but I don't want to be the reason one design is chosen over another. I don't work on the compiler and think that others probably have better insight and can judge these alternatives in a fairer way.

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

It's a compromise, so we've almost got insignificant whitespace... but just enough to keep a sane default and not have end everywhere

view this post on Zulip Niclas Ahden (Feb 11 2025 at 11:38):

Yeah, I think it's a strong option and less verbose than some of the other options. I really want "low friction" when I have an idea and I want to type it out. For some reason, I can sometimes think of something cool but then when I need to start typing it out I feel like "oh, that's a lot of typing" and I go do something else. Perhaps I should just fix my mentality/motivation, but this is one of my reasons for wanting light-weight syntax.

PS. Please don't call Programmer Protective Services on me or anything. I'm working on my issues, I promise! I just ordered a keyboard from Sam to make typing a breeze! (once acclimated, of course :sweat_smile:)

view this post on Zulip Anthony Bullard (Feb 11 2025 at 12:26):

So basically.....delimited blocks have no whitespace significance, but undelimited blocks do (in the sense of just make the body a little bit more indented), and we are making a few existing blocks delimited that weren't before.

view this post on Zulip Anthony Bullard (Feb 11 2025 at 12:29):

I think this is definitely a compromise, but not one I feel fully satisfied with. But I can move on with it. I struggle with large hanging indents, but maybe that's a hint to me that I should NOT have so many levels of logic in a single function...

view this post on Zulip Niclas Ahden (Feb 11 2025 at 12:47):

I share your concerns Anthony, and this discussion makes me evaluate my style too. I frequently have nested functions, e.g. for building up HTML templates, and I don't want to pollute the top-level with useless stuff. This leads to hanging indents. I started thinking that perhaps I should break things out into separate modules more so that I can use the top-level without polluting. Maybe there are other solutions, but it'd take some experimentation. All that to say, it's a compromise, and perhaps our coding styles influence the ergonomics of it.

You expressed how you think of the proposal. Here's my way: "control-flow needs end; functions do not". Anyone please correct me if I'm wrong.

view this post on Zulip Joshua Warner (Feb 11 2025 at 15:24):

w.r.t. the concern that missing an end is annoying, I think we ought to be able to fix that up in most cases. Specifically, if all you're missing is one (or more) ends and indentation is consistent, we can infer the position of those ends from indentation and inject them where appropriate (I want to do this with braces too!).

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

Niclas Ahden said:

You expressed how you think of the proposal. Here's my way: "control-flow needs end; functions do not". Anyone please correct me if I'm wrong.

This sounds right to me.

view this post on Zulip Norbert Hajagos (Feb 11 2025 at 20:18):

I can't gauge how the significant ws inside lambdas idea would fare, but I think if we go to the length of having the control flow constructs be closed with an end, it feels appropriate for lambdas as well.

main! = |_args| do
    Stdout.line!("hello, world!")
end

This is as aesthetically pleasing as significant ws to me. I also like jumping to the "pair" of a token in tree-sitter enabled editors, (% in vim), which is pretty niece (more so that the "goto the previous function definition", which would eliminate the need for the end). I like seeing where a nested named lambda ends.
I'd also add that in js () => expr is valid for expressions, but for statements, you would use () => { statements; }. Not that js is the peak of language design, but I don't think it would be alien for people to say 'you can skip the do...end for "small lambdas" '.
Out of the suggestions, I think this approach is the safest that could succeed. The lambda-significant ws approach depends on a heuristic that we find the optimal significant ws – delimiter balance, thus seems more of a plunge. It could totally work out, but I'd rather take the consistent route. But againd, I also like the look of it, like Anthony.

view this post on Zulip Norbert Hajagos (Feb 11 2025 at 20:22):

Voicing my opinions on this is also a means to justify myself reading ~800 messages on this topic. You have nicely compensated for the past few days of relative quietness on this channel, which I can only :applause:

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

I split off an idea in #ideas > braces syntax

view this post on Zulip Notification Bot (Feb 19 2025 at 05:19):

JanCVanB has marked this topic as resolved.


Last updated: Jun 16 2026 at 16:19 UTC