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
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:
the basic idea is:
when, if, for, while, inline lambdas, and inline defs, you don't need to do anything if they're surrounded by parens, but if they aren't, then you need to use the end keyword at the end to show where they endend at the very end of a top-level declaration, because we already know where those endgoing 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.
which feels like a pretty small delta to get the benefits of:
that said, maybe I'm missing something!
curious what others think of the idea in general, and also whether I've missed something
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.
here's the full delta in roc-realworld: https://github.com/rtfeldman/roc-realworld/compare/end
Wow, 5 lines!
When you put it like that...
it only needed 5 ends across about 1k lines of code
Sounds like a pretty major upside... with very minor downside
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
Sam Mohr said:
When you put it like that...
yeah I was surprised too!
I wish I'd thought of this years ago :sweat_smile:
Is the rule confusing around when end is required and when not, i.e. inside parens
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
so they'd be short-lived haha
@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
(no idea how much that would or wouldn't generalize though!)
I worry that it wouldn't generalize well
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.
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.
interesting! anything you can share?
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
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.
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.
here it is with parens instead of end https://github.com/rtfeldman/roc-realworld/compare/parens
and the rule I mentioned about top-level decls still needing indents
Yeah, I prefer that.
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
so I updated it and now it's just rearranging things and the only place that needs parens is the for loop
and everything else doesn't need any other changes
that is extremely unexpected :sweat_smile:
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.
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?
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:
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).
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
ok nice!
one idea: we could say that return "ends a block"
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!(...)?
and a warning
so instead we could interpret that as ending the block, and leave it as-is
in that case, I think the only change would be that the nested when would need parens around it?
I'd definitely prefer end over parens if we go this route.
Anything we can do to avoid nested parens is ideal
Richard Feldman said:
in that case, I think the only change would be that the nested
whenwould 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.
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
Which is a reason I'm a proponent of this proposal
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
@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:
thanks!
to summarize, the revised idea is:
when, if, for, and while all need to be either surrounded by parens or followed by a "block ender"return, and crashend as a block ender (just to avoid parens) but this isn't a strict requirement of the idea_ = (we could say having a ; at the end of them is syntax sugar for beginning them with _ =). Alternatively, we could say that the bodies of top-level declarations still have to be indented by some amount, but the amount doesn't matter, and it no longer has to be consistent from line to line within the declaration. (Closing braces in top-level declarations could still be outdented, like today.)cc @Joshua Warner and @Anthony Bullard since I know you've both had thoughts on whitespace significance in the past!
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)
Ok, I think my brain is melting right now
I'll probably have to come back and ask questions about this in the morning
And I'm sure Joshua will have to as well
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)
I like the simplification of for ... in ( ... )
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:
well, either that or end being necessary after top-level declarations as well as ending if/when/etc.
or parens/brackets of some from around them
yeah
I would say that all of those options sound preferable to the inconsistent top level declaration rule
really? haha
Hmm maybe I’m misunderstanding your proposal?
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
any of those alternatives would be much more invasive
Having a clean and consistent syntax seems more important than making the delta to the existing syntax smaller, no? 
those two seem to be in tension though
adding a bunch of symbols all over the place would definitely seem to make the syntax less clean
Making the rules different for the top level and nested levels makes the syntax even less clean IMO
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.
hm, actually statements don't come up in that code base
so I guess the rule could be that ending a line in ; is syntax sugar for beginning it with _ =
that would come up pretty often in rocci bird
but that does seem to be the only way rocci bird (773 LoC) would be affected
@Joshua Warner what do you think about multiline strings?
From a parsing and formatting perspective, I would like to get rid of them ;)
Make a lot of things unnecessarily more difficult
so, an alternative design for them would be something like:
my_multiline_str =
"""line 1
"""line 2
"""line 3
but that makes pasting a lot less convenient
it does have the upside of making the parser more error-tolerant though
can you think of any reason this design wouldn't work?
Ooh yeah that makes things easier on the lexing and parsing side. Formatting is probably a wash in terms of complexity.
With that change I think you could almost lex in parallel if you wanted
Eg split the file into n chunks and lex on separate threads. Just need to find the nearest line boundary.
You’d have to also require that string interpolation expressions not contain new lines
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.
hm I think actually all inline defs would need to end in ; in this idea
edit: nm, I realized that's not necessary!
I think the """" on each line" design makes more sense in a world where we support importing files as strings
because if you really want to paste something big, that might be the nicer option anyway
in fact, we could consider not supporting """ at first and see if there's still demand given import-as-string :thinking:
import-as-string doesn't solve the case of wanting to do string interpolation to a large string
ah good point
unless we wanted to make it fancier :stuck_out_tongue:
but that's neither here nor there haha
Haha I was thinking exactly the same thing. Something something roc-template-language
Anyway
point is, there are multiple paths we could take to get to not taking indentation into account at all, if desired
I guess a use for ; would be if-without-else:
if foo then
bar!();
something
edit: never mind, that wouldn't work!
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
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.
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.
Richard Feldman said:
well, either that or
endbeing necessary after top-level declarations as well as endingif/when/etc.
Do this and change static dispatch from . to : and Roc begins its Lua story arc.
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:
- "block enders" include the end of the file, the beginning of a new def,
return, andcrash- optionally, we could introduce an
endas a block ender (just to avoid parens) but this isn't a strict requirement of the idea
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.
The big issue here is that Roc decided to support statements - i.e., expressions that occur mid-block that aren't in a definition
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
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
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.
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.
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:
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!
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.
I think this may be one of the few syntax threads whether every combination of preference has been expressed :laughing:
so we should definitely keep the status quo, and also change it, and the thing we should change it to is every different option.
I don't feel like I understand where this thread stands, unless I take your summary above as the definitive statement Richard
I don't understand where it stands either haha
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).
so basically we've seen:
end instead of parens, and also opposition to endend or parens, and also opposition to curly braces (solving this problem by introducing curly braces for all blocks feels to me like hitting an ant with a sledgehammer); as sugar for {} = in order to reduce the need for end/parens, and also opposition to ;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
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
I would include the multiline string syntax change with this
yeah it's almost just that, except nested when still needs something extra (could just be parens)
and nested defs, although those are fairly optional imo
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
Why does nested when need something extra? Its grammar is:
"when" EXPR "is" [PATTERN "->" BLOCK]*
Where block is:
[DECL]* EXPR
Seems to be very well defined and fully delimited unless I'm missing something
when foo is
Bar ->
when x is
Baz -> 0
Blah -> 1
Stuff -> 2
which when does Stuff -> 2 go with?
(unless we use parens or end to disambiguate)
(or significant indentation)
Oh shoot, of course
So a when in the final position of a block inside of a when
It seems that is rare enough to make requiring parens to be reasonable
Even if I think that having end would be more consistent
Richard Feldman said:
- support for removing indentation-sensitivity, which introduces ambiguity into the current grammar that (in the absence of any other changes) would have to be resolved with lots more parens
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).
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.
I don't think there's ambiguity in significant indentation - we just say that only tab characters are allowed for indentation
the only reason to consider both tabs and spaces would be for error tolerance/recovery
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
whereas with whitespace-insensivity, if you forget a semicolon or nest things in a particular way, it can "look right" but be broken
unless you require a lot more explicit delimiters than we do today
e.g. braces around every block, or end after every block like Ruby
Richard Feldman said:
e.g. braces around every block, or
endafter 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
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
I really think when...is is the only existing syntactic construction that would require an end
Obviously for will need it as well when it's implemented
yeah, either end or surrounding it in parens
(and only nested whens would need that treatment)
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
I thought end is nice since:
A) it reduces paren overload
B) The rest of the syntax for the construction is keyword driven
But yeah I agree it could be an optional piece of syntax for explicit delimiting
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?
Don't do this
JS has this exact problem and it is MISERABLE
I literally used to hit this at least once or twice a day
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)
It's very similar to Zig's "expected a , after statement" when you forget the . in front of an anonymous struct literal
Braces with implicit return means it would have to be
foo(|x| { { x } })
To return a record
foo(|x| { x })
returns x
Yes I agree - no way I'm down with that
I'm not sure how confusing/surprising the experience would be when forgetting the edge case of nested when
e.g. if I write this:
when foo is
Bar ->
when x is
Baz -> 0
Blah -> 1
Stuff -> 2
I would 100000000000% prefer the world where when (and for eventually) requires end
the formatter would change it to:
when foo is
Bar ->
when x is
Baz -> 0
Blah -> 1
Stuff -> 2
so I think I'd see the problem pretty quickly
The formatter would definitely tell you what the problem is :rofl:
And more than likely there would be one or more type errors as well
right, although if I'm not using the formatter, I might not understand the type error
foo not being handled exhaustively, and x being matched against a bigger union than expected
For sure
I think we could probably detect this case and make the error more helpful
But it wouldn't be all cases especially with type inference and no annotations
This is why I'm for just for a little more typing to make it 100% consistent
actually the type error might be fine if it showed both branches in the snippet
e.g. "these last two branches don't agree":
when x is
…
Blah -> 1
Stuff -> 2
Yeah, possibly
Just like a -C1 in grep
A little context helps :-)
I think showing up to the previous and the next non-blank line would be helpful 98% of the time
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!
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:
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?
"Whitespace insignificance" typically means "indentation significance"
Not that no whitespace matters at all
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
And a good formatter should probably have some hard and fast rules about when a statement becomes multiline
here's what it would look like if all whens and ifs ended in end: https://github.com/rtfeldman/roc-realworld/compare/end-always
I'm gonna catch some heat for this opinion, but I think with syntax highlighting that would look HOT
And that is all the disambiguation we need right? We don't need lambda bodies to have end?
So we don't go full Ruby/Lua/Elixir?
that's correct, as long as we're okay with {} = or ; for statements
if we want to avoid that, then we need end for all blocks, including lambdas
I actually personally like that version too
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
especially since when gets double-indented
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,
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:
Sweet, I can deal with a semicolon if I need to do a Stdout.line!
The _only_ bad part about that is that means that ; would be introduced pretty early in the tutorial
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
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:
FWIW I'd prefer if we never drop _ =s
I always feel icky doing when inside of another expression today
JanCVanB said:
FWIW I'd prefer if we never drop
_ =s
I wouldn't hate this either
I think it muddies the mental model for very little gain
we could certainly try it
could end up with a lot of _ = log.err!("this should never happen") :sweat_smile:
Wait, as opposed to what?
as opposed to log.err!("this should never happen");
or {} = log.err!("this should never happen")
Love the first one!
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
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
If we add whends, I'd prefer the former to the latter.
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
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
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
If we add thends, I'd prefer the former to the latters.
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
like with {} = I know it's not discarding information, but with _ = it might be
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
Can't believe I'm saying this, but I think I prefer ; here
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.
This is the one I'm not sure I love:
verify_total_size_of_chunks!(...)?;
I don't mind that, although maybe that's because I'm used to it from Rust
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)
it's sort of like "hey this is full-on imperative right here"
"we have entered the Imperative Zone"
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
Regarding the naming of that keyword, I expect folks to expect that to act like Haskell's do
do could alternatively mean "this is a delimited block, where statements can exist alongside other things but can't return anything"
It pretty similar to do in haskell, where you do all of your IO code
I think syntactically it looks lovely though
But in Roc, any foo! can do IO, regardless of its return value. if start! then can do IO.
I think the more common case would be logging
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
where you just have one statement and then you move on
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?
Richard I was thinking that do as a block could also be used for for
And since for is basically a statement (are we planning on a return value from it?), it would require a do block
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.
I think to allow someone to copy/paste code around without worrying about it , in the spirit of "inform, but never block"
I argue that even if Roc needed zero syntax changes to "make indentation insignificant", we shouldn't do it.
...unless we're only talking about expanding the formatter's input domain without expanding its output range.
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
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
Are you using the phrase "whitespace significance" as a synonym for "pre-formatted indentation parsing"?
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.
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?
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?
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 ;.
Does that mean that of the following possible goals
this topic is only discussing goal 6?
Or are we using a specific technical definition of "insignificant" that differs from the casual meaning of "arbitrary" / "anything goes" / "personal preference"?
the formatter would 100% enforce indentation no matter what
there's no world where it preserves whatever arbitrary indentation you chose
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"
right
Where 2 spaces > 1 tab?
No, where "if line one is space + tab + space, line two needs to be space + tab + space + space/tab"
For stuff like when
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
Maybe that's out of date from prior discussion in this thread, though
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
(in this proposed design)
Does that mean that an equivalent topic name would be "fully automating indentation"?
To me, languages with "insignificant whitespace" are JSON and Java(almost).
Artur Domurad said:
And I would say that
endis 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
Python is more popular than Ruby, but also way more people complain about its syntax
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
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
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
aside from string literals needing to be single-line, as they are in all mainstream languages
like for example I think this would parse:
x = when foo is A -> 1 B -> 2 C -> 3 end
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
All of my fears are eliminated. Thank you.
here is a potentially interesting way to think about the do idea: what if do .. end is syntax sugar for ( ... )?
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
so here, instead of end we have the x = serving as a block-ender
and then we have:
for x in y do
whatever!()
end
...being equivalent to:
for x in y (
whatever!()
)
Is that just to avoid {} =? I like {} =.
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
)
JanCVanB said:
Is that just to avoid
{} =? I like{} =.
yeah, it would be an alternative to {} =
so far I've seen more opposition to {} = than support for it, but I could be wrong :big_smile:
unlike _ =, we could make there be a warning for do Stdout.line!("...") in the case where you're ignoring some useful output
Would this generalize to do1+2end*do3-4end?
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
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,
endor significant indentation" I think the evidence suggests it'send
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!"
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
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,
endor significant indentation" I think the evidence suggests it'sendEvidence 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
endis, 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:
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.
I think it's a subtle distinction, but I don't think the responses are balanced here
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
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
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
Richard Feldman said:
I think that's a fair summary of the general controversy around significant indentation:
- proponents like it and say it's less cluttered without losing clarity
- opponents feel that indentation shouldn't affect the meaning of code
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.
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
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
I don't think the justification matters to them :big_smile:
they see it as unjustifiable at face value
in contrast, I don't see people having this visceral reaction to thinks like end or braces
the worst-case scenario I've seen is a mild-to-moderate preference for something else
Richard Feldman said:
there was a guy in the front row who hissed every time I mentioned significant indentation
bro
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.
Enforcing coding style seems to work well for go
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.
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.
yeah I totally hear that!
to me, changing to braces is what feels like the "bridge too far" to me :stuck_out_tongue:
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
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
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
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
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
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
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?
If anything, I think people say that Ruby looks "scripty". But that seems like a mild opinion :shrug:
I see 3 distinct types of end above - whend & thend for enabling auto-indentation and doend for reducing parentheses.
There's also do for reducing {} =s.
There's also ; for reducing {} =s.
There's also alternative syntaxes for multiline strings, for enabling auto-indentation.
There's also requiring a block to explicitly end (with/without introducing end) for enabling auto-indentation.
What does whend and thend mean? Are you saying those are keywords?
I'm being silly and contracting:
whend = when/endthend = if/then/[else if/][else/]enddoend = do[/end]I just think it would be good to write down a concrete proposal for "Remove Significant Whitespace from Roc"
High-level:
whens must end in end.ifs must end in end.For current usages of statement expressions mid-block there are some alternatives
do syntax:
do is available as syntactic sugar over {} = EXPRdo 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.dos are a valid statement; syntax:
; is a statement terminator that is syntactic sugar for a statement expression, converting it to {} = EXPRDisallow mid-block statement expressions:
{} = 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.
Questions I have:
do valid inside a do...end block?do...end block?Anthony Bullard said:
High-level:
- All
whens must end inend.- All
ifs must end inend.dois available as syntactic sugar over{} = EXPRdofollowed by block completed withendis 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.dos are a valid statement- A block will always be any sequence of definitions followed by a single expression
- The parser will not pay attention to newlines or comments at all.
- 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
That's a great question!
I don't think I understand why that would be needed
But Richard always thinks of a case I'm not thinking of
Anthony Bullard said:
High-level:
...
dois available as syntactic sugar over{} = EXPRdofollowed by block completed withendis 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.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").
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
JanCVanB said:
Anthony Bullard said:
High-level:
...
dois available as syntactic sugar over{} = EXPRdofollowed by block completed withendis 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.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 {} =
I'll clarify the above post
Ah, yes, I'm wrong - I keep forgetting those =-less expressions are allowed and I dislike them.
I think Richard likes having that syntax quite a bit
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
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
Would one be free to not use do (for instance around that entire function body) and continue using _ = for statement expressions?
Of course, do is pure sugar
I mean essentially in terms of style tastes, this does allow those such as @JanCVanB to continue using their preferred syntax.
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
So it just depends on if the formatter will be made to be "opinionated" about this
To allow myself a personal opinion, I think the above is very nice looking and very consistent and very unamibiguous.
I wonder if there's a single-character version of {} = that many folks would prefer...
! = W4.text!("Click to start!", ...
I just don't like useless assignments
Part of the joy of FP to me is that everything is assignments.
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!
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.
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.
Does do Stdout.line!("...") not imply that to you?
I actually like that, hence my insistence that do is a separate proposal from do/end :laughing:
dewend
endo
do/od :laughing:
I expect any of these dos would pair naturally with adding a void return type.
That doesn't seem necessary, seems like {} does the job quite well
I say the same about {} = and feel alone :laughing:
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
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
so maybe the do ... end idea is a mistake
looking at the semicolons version, it feels like the semicolons are pretty easy to miss
I can imagine a lot of " :man_facepalming: I forgot a semicolon" experiences when writing statements
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
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
that said, I definitely prefer not having to write def or let :sweat_smile:
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
Richard Feldman said:
that said, I definitely prefer not having to write
deforlet:sweat_smile:
we are planning on having var...
yeah but that's an edge case
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
)
Richard Feldman said:
another possibility: put
endat 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"
I'm pretty vehemently anti-parens here
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.
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.
|...| EXPR
and
|...| do
STATEMENTS
...
EXPR
end
would be a perfectly consistent approach IMO
Brendan Hansknecht said:
Can someone ELI5 to me why
endis 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:
and
)on its own line looks funny
hmm. I guess I am too used to any type of bracket on its own line
So that feels irrelevant to me
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.
I don't like those either :big_smile:
Also ')' here adds yet another bit of significance to that symbol
Like is this going to be tuple item? or a function body?
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
I would definitely prefer end at the end of lambdas, than do before each "statement"
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)
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
yeah I just wonder if that's the least bad option
I just always am going to argue for the most consistent option
I get that, but we've gone down that path and hit a bunch of downsides
so I want to re-explore the downside of that option
Sure
people don't seem to complain about spaces being significant around keywords, e.g. return needing to have a space after it
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?"
to me personally, that feels like it's in a totally different category from significant indentation
where I need to match things up
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
I think I just don't know if this is really only a top-level problem
I think its also a problem with a sequence of defs inside a def or lambda body
hm, that's fair
If it were I'd be up for it
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
same with if and either end or else
inside a nested lambda it would be a problem though
Yes or nested in a def. Which I think is little silly without shadowing anyway
But we support it
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
the argument against them would be that they aren't necessary
and also that they're making the common case look noisier in order to accommodate the less-common case
I know I would just take an end
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?
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
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:
end at the end of when (or parens around nested whens, but I don't think we should do that){} = or _ = for statementsI don't think there are any other cases that would be absolutely required to be different, unless I'm not thinking of something?
end if required at the end of if/then/else as well?
I don't think it's required for if/then/else
as in, I don't think there's any ambiguity there
so using it there would be more for consistency
So actually with optional else branches, there is ambiguity
Right because you HAVE to have an else. It's block would always be the end (or a return)
Joshua Warner said:
So actually with optional
elsebranches, there is ambiguity
oh that's a good point - we do want to introduce those
Optional else always ends in return, no?
nah, could be a statement
Oh shoot, yeah then you would want end for those
So it's like, do you always want end, or only in the stead of else?
to me, adding end to if and when feels like a totally fine change that I don't mind
in isolation
the problem is that I don't like {} = or _ = or do or ; nearly as much as status quo for statements :sweat_smile:
Sure, then new multiline strings, and do is a separate convo
Hehehehe
oh yeah I guess also for and while need end, but again, that feels fine to me
I think if I had to pick one of the alternatives for statements I'd lean towards do
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)
Or just all blocks need an end delimiter, including lambda bodies
and because _ = discards information and I like reading do more than I like reading {} =
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
I agree
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
Just laying out the solution space
unlike multiline lambdas, which do feel significantly less nice to me if they require end
do seems like reasonable sugar
And we don't have to be so opinionated in the formatter to force null defs into it
unless we feel like that creates a rift in the ecosystem in terms of code style
I do think in that world it might be nice to find a different keyword for loops
like if do means {} = then it would be weird to also have it in for x in y do
e.g. maybe for x in y loop might be better
Maybe, but won't a for be mostly for statements?
Or really exclusively?
oh interesting
yeah I guess arguably it is still being {} = :big_smile:
that's kinda cool
That's why do...end makes sense as a "no need for dos on statements in here"
It's just a block where syntactic sugar is applied to all child statements that are null defs
Direct children that is
do EXPR is a "imperative statement" whereas do...end is an "imperative block" where imperative statements do not require a do modifier
If I wasn't sick I'd design a nice 4 across spread of nicely highlighted code snippets showing the different alternatives
but light-hearted Zulip banter is the best I can do at the moment :-)
ok, so then I think the frontrunner concrete proposal based on all of this would be:
if, when, for, and while now end in enddo becomes syntax sugar for {} = (which is now required for statements)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
I think this needs a do:
draw_animation!(state.rocci_idle_anim, { x: player_x, y: player_start_y + shift })
my overall thoughts on this compared to status quo:
end - there are some aspects of it that I prefer to status quo, and vice versa. Overall, pretty much a wash for me.do, but I think I'd get used to it, and I don't have any alternatives that I like better.One thing I like about this is that the do keyword draws more attention to where the effects are occuring than the ! suffix
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.
@Niclas Ahden I think you have 90% of the world's Roc HTML code samples in client code ;-)
Maybe 99%
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
This sounds delightful!
my = Hot.take |this!|
do this!
end
_ = my_concerns(previous_proposals)
for change! in latest_proposal do
change!
end
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
it's hard enough to figure out just the indentation piece on its own :sweat_smile:
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:
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)
(there's a when inside the first if that's missing an end)
the very last end there is an example of one that I like
as in, I prefer it to status quo
because I always get a little nervous when there are big outdents
like "is this actually at the correct indentation level to pick up where the previous thing left off, or did I outdent too far?"
it's not a big thing, but in that particular case I like it
Anyone here strongly against end if and end when and end for and end while?
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.
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
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:
I do think PNC + static dispatch will end up being a net subtraction of LoC for what it's worth
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)
when doing roc-realworld and some others
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.
Yeah, I'm really excited to try SD! It looks super clean :)
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.
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!"
I totally hear that!
naturally, I'm also trying to balance finding solutions for longstanding concerns :big_smile:
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.
yeah, also a great point
@Brendan Hansknecht also I know you're not a fan of end in general - how strongly do you feel about that?
@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)
It's a small bit to track in the parse ast
And I would do this specifically only for Apply
JanCVanB said:
For LOC trimming,
if long_fooooooooooooooooo then long_barrrrrrrrrrrrrrrr else long_bazzzzzzzzzzzzzzzz endcan always be
foo = long_fooooooooooooooooo bar = long_barrrrrrrrrrrrrrrr baz = long_bazzzzzzzzzzzzzzzz if foo then bar else baz endwith 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
Not very. As I see it:
end and have only significantly used similar things in shell scripts like fiend more visually mixes into code than parens/braces in a way I find harder to parse/noisier.end would likely lead to the most similar to current syntax, but that will feel really inconsistent.Yes, end doesn't have to be on a newline technically
But I'm totally open to it being the right call
fwiw I had never used end prior to Ruby and I got used to it pretty quickly
Good to know
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)
we must attract the cobol programmers :100:
that is the main motivation
LOL
Good use of the 100 emoji, since there are only 100 left
They'll need a job once all that COBOL goes away - why not in Roc?
I've actually heard that cobol programmers are in huge demand
They get paid a lot
Oh yeah, they do
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
I knew that when I made the joke, but didn't want to let reality get in the way of comedy
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
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?
Why has noone invented a language that compiles to COBOL?
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
@Joshua Warner @Luke Boswell curious what you think of this proposed design when you get a chance!
(I think everyone else who's been active in the thread has weighed in?)
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
in the current proposal, this would no longer Just Work
I wonder how much weight that should be given in regards to do vs other alternatives like end at the end of lambdas
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 !
yeah I was thinking about that
Would be nice to always write the same thing for statements
a problem with that is that the formatter can't enforce it
like it can't add a do
because it doesn't have type info, so it doesn't know when it would be appropriate
It kind of can
It'd have to introspect for a ! at the top level
I feel a void beneath us... (half-joking)
Which would mean we can't do it if you incorrectly forget to put a ! in your effectful function name
the ! function can still return a Str or something
and it doesn't have to be annotated
Then maybe it's just a warning
I think a warning is fine
I think this is a good argument for end at the end of lambda bodies. But I think I'm alone there
I do worry that the farther we get from {} = the more surprising {} will be in the type system.
Richard's point of lambdas being much more numerous than statements is the main thing
I'd be really surprised by that
You just need to hover Stdout.line! to see it's typed Str => {}
If we end up in a world where we have {} = everywhere I wouldn't cry too much
Anthony Bullard said:
I think this is a good argument for
endat 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:
maybe I'd get used to them though
another good argument for them is that it's probably the gentlest learning curve
But I think that it would also mean the same on nested defs, like I said above
Or we just disallow them
because then the proposal is:
if, when, for, while, and lambdas now end in endand that's it
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"
because .map(|x| x + 1 end) feels totally unnecessary
but, to Niclas's point, now we're adding end in a lot more places
I think frequency of use can trump consistency, it's why ? is better than try!(...) in Rust
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
Random note: It might be surprising to learners that if you add a return value to an effectful function you no longer do it.
yeah, could be
so
x =
y = 1
y + 2
would have to become
y = 1
x = y + 2
yeah I think it's fine if nested defs need parens
I think it'd be weird to say "you cannot do them in this language at all" but they're pretty rare anyway
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"
But them we have ambiguity with ()s. It not means "I delimit a block" and "I can wrap an expression"
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:
Oh, and "I am a tuple"
this would be an example of wrapping an expression
Feels like do-end is better for that reason Anthony
I like nested defs in languages where I can shadow, but not Roc
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
feels like the "end at end of lambda" design is strictly better for learning curve
I just want to be clear:
x = (
y = 1
y + 2
)
Is not really wrapping an expression, that's wrapping a block
Richard Feldman said:
feels like the "
endat end of lambda" design is strictly better for learning curve
I agree with this
But this isn't record builders or effectfulness, it's just "big block has do"
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
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
and you can already put parens around it today if you want
Oh right, a Defs expr
Which is not intuitive at all to those not familiar with the ML family
I know that's really a let...in
agreed, but again I think it's okay if people don't know about these because it's such an edge case
like I don't think any beginners are thinking "oh no, how do I do this?"
and if they are, they can always extract a function etc.
If we add lambda-end (AKA "functiends"?), what other proposed syntax changes would we no longer need?
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.
multiple ways to get unblocked even if you don't know it can be done
So the real issue would be
x =
y = 1
doing_something!()
y + 2
right, that would need parens in this design
JanCVanB said:
If we add lambda-
end(AKA "functiends"?), what other proposed syntax changes would we no longer need?
do and do..end
@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)
Richard Feldman said:
right, that would need parens in this design
How is that different than statements elsewhere?
I don't think it is different :big_smile:
statements elsewhere also need either parens or an end
:-)
I just don't think we should introduce do ... end for that case when parens are sufficient to solve the problem, and already work
So you are saying that parens after lambda args is equivalent to ending the body with end?
yep!
Ok
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
e.g. (if foo then x else y)
just stylistically I'd prefer end when there aren't already parens involved
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
I hear that, I just think it's so uncommonly used that it's not worth introducing special syntax for
Like z = (y = x + 1 y) being an expression like z = (if foo then x else y) is just ... weird textually
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"
Ok, that sounds fair
I guess I just never thought about how weird the Defs expr is with the way it's syntax looks
But it's good syntax sugar, which languages like F# solves with whitespace significance
(But you can use OCaml syntax too if you want - and not worry about indentation)
Ok, going to rest my brain (and my hands)
Richard Feldman said:
Luke Boswell thanks!
what do you think of the alternative design where we don't have
door;but all lambdas needendat the end? (including top-level ones)
Another idea... what if all lambdas require a return?
Does that also solve our problem?
@Luke Boswell Don't you dare..... (Though it would be popular to some)
I like it. I think it's really beginner friendly
(Also no, unless you want all blocks to have it) Actually maybe
Yeah, that's the way I'm leaning
@JanCVanB May come for you :rofl:
hmmm
I definitely had not considered that, because I like having implicit return at the end of lambdas :big_smile:
but I think it would be an option
in the sense of like
"either you have a close paren or a return"
so .map(|x| x + 1) could still work
and then top-level functions would probably always use return
And then you have to remember to add return when you pull your lambda out to a def...
my knee-jerk reaction is that I don't like it, but maybe I should give it a chance
My knee is jerking really hard
objectively:
end or ); or _ = or {} =oh wait then you'd have to do return {} at the end of functions that don't return anything :joy:
I think that pretty much rules out that design
Unless you make void a thing...
not because of types, because you need a way to end the block
Yeah, I guess
otherwise the parser thinks the next top-level declaration is an inline block
so I think end at the end of lambdas now feels like the most beginner-friendly design
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?
anything with the type => {}
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 {}.
or perhaps more obviously, it would have to be:
main! = |_args|
return Stdout.line!("Hello, World!")
@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
I agree that it's copium, and I would vote to change the formatter to one line in between :)
Elm's?
Yeah
yeah, me too :joy:
So for Roc I'd prefer the same: one line between functions (no end)
yeah same, all else being equal
what do you think about the end for lambdas design in the context of the other tradeoffs?
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.
yeah, end at the end of lambdas would mean not needing do or ;
which I think would be easier for beginners to learn, plus experts wouldn't trip over needing to add do or ; in some places
Richard Feldman said:
what do you think about the
endfor 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.
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
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
might be fine to say those have to go in parens
Overall I think the current situation is better than what's been described here. Probably because:
end makes the language feel childish or something... idk. Ah! THIS is why https://www.qbasic.net/en/reference/qb11/Statement/END.htm QBasic was my first language, and then I was a child, and it has END. Weak argument, but yknow.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({})?
Are there any other strong pros than these?
- copy/pasting working better because indentation doesn't matter anymore (outside of perhaps needing some indentation for top-level defs, plus I guess multiline strings?)
- better error tolerance in the parser - no longer having to deal with all the edge cases around "what if a mix of tabs and spaces are used to indent?" - we just blow them all away and replace them with tab in the formatter
- reclaiming some strangeness budget. 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 :
those are the ones I'm aware of :+1:
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.
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!
to put it out there as another concrete alternative to the status quo (again, setting aside triple quotes), here is a different possible design:
if, when, for, and while all need to end with end=) has to begin with a space or tab, but it doesn't matter how many. (Like today, lines consisting only of closing delimiters never need to begin with spaces.)compared to today:
end in our conditionals, which increases line of code count but makes some things (imo) look nicerreturn, which would also unambiguously end it). I'd say that inline named lambdas come up rarely enough, especially after for and while reduce demand for tail-recursive function helpers, that I'm okay with this.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.
when I hear the specific reasons people list when they say they dislike significant indentation, none of them apply to this design
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"
(I don't know this is how it would go down, but I don't know of any language which has tried this experiment)
Pretty minor, just wondering, do for and while still have do?
yeah
but it's just their version of is and then
nothing special
agreed
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.
honestly I think it would probably work to just not even mention it :stuck_out_tongue:
like just write the code and never mention that things have to be indented
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
We could even say "Roc has neither significant nor insignificant whitespace - it has automated whitespace" (despite the formatter's input domain being ws-sig)
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
I literally think I would try not even bringing it up in the tutorial
and see what happens
Can for and while have return values?
they shouldn't evaluate to expressions
like {} = for ... should always (conceptually) type-check
not saying that syntax should or shouldn't be valid
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
As with the ? expression-return vs. function-return discussions, you can always wrap for in a zero-arg lambda to make it return locally
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
whereas the convenience gap between for and a lot of tail-recursive functions and walk variants is much bigger
A thing I like about your latest proposal here is that end is a keyword that matches with other keywords but not vertical bars.
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:
if,when,for, andwhileall need to end withend- each line in the body of a top-level declaration (after the
=) has to begin with a space or tab, but it doesn't matter how many. (Like today, lines consisting only of closing delimiters never need to begin with spaces.)compared to today:
- we no longer need to track indentation level, so copy/pasting Just Works everywhere, tabs vs. spaces don't matter, and you never need to make indentations agree
- we have
endin our conditionals, which increases line of code count but makes some things (imo) look nicer- 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. I'd say this comes up rarely enough that I'm okay with it.
Would you mind making an example with this syntax? Or is it literally just the same as current with the addition of ends?
yeah it's literally the same
except it's more permissive, like if you make your indents go all over the place the formatter just fixes them
as long as there's some indentation
That's nice.
I imagine it will also helpful if we are having the formatter use tabs everywhere for indentation.
Mobile keyboards rejoice. 20+ year old terminal text editors rejoice.
Just newline-space-coooode-newline-space-coooode-savefile.
I think this "any-indentation" design is my favorite of the ones we've discussed.
comparing it to the alternatives:
end after all lambdas feels like it adds a lot of noise and LoC for benefits that seem mostly a matter of principle rather than practicality. Like "yes paste still Just Works in the other design, but in that design technically indentation is still A Thing even if it's inconsequential in practice." That doesn't seem like a great justification to me._ = or {} = or do or ; not only adds noise, it will also predictably make the language harder to learn for beginners, plus even advanced users will probably forget from time to time and get annoying errors reminding them to do the chore of adding do or ; or whatever.end to be omitted at the end of conditionals would be fine in some cases, but I think the consistency and muscle memory of "just always put end at the end of conditionals" seems better
- 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. I'd say this comes up rarely enough that I'm okay with it.
Could you explain this a little further?
# 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)
This is a really common pattern I've used, particularly to close over some args and make helpers.
Also are we losing the property that functions and lambdas are defined the same everywhere?
that's correct, that's how it would look
I don't think this affects the property...they're still lambdas, it's just sometimes you need parens to disambiguate
which is something that comes up in various different places, e.g. when using some operators together with one another
Is typing this a parsing error?
top_level! = |arg|
adder = |num| num + 2
I'm not opposed to this, just wanted to clarify.
yeah, it would need to be:
top_level! = |arg|
adder = (|num| num + 2)
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)
The former looks worse IMO, but is more consistent
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 (
But the second one doesn't work the same as list.map(|arg| arg + 2)
The main thing is that ( after the second |
as with triple quotes, I think there are a variety of possible designs here and I'm not really worried about it :big_smile:
Well, I'm onboard
cool! I'm curious what @Joshua Warner and @Brendan Hansknecht think of this design
I'd like to congratulate @Anthony Bullard for his 2.5 month victory:
that is, this design:
Richard Feldman said:
I think this "any-indentation" design is my favorite of the ones we've discussed.
Hmm, would type annotations mess with lambdas?
main! = |args|
inspect = (|arg|
bytes : List U8
bytes = arg.bytes()
bytes.inspect()
)
for arg in args do
Stdout.line!(inspect(arg))
end
Can we always distinguish when a type annotation ends and when the def starts?
I'm thinking of the (|x| when x is A -> 1 B -> 2)
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"
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.
sure, but it still works haha
it's not disallowed, it just doesn't look as nice as today
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
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
@Richard Feldman How would you respond to someone genuinely asking "why not just braces-wrapped blocks?"
"is map(|x| { x }) returning a record or no?"
Fair. What if "... multi-line* blocks?"
it's the same problem
y = {
x
}
already means something
as Anthony noted, it's already a point of confusion in TypeScript even for advanced users
And we don't want lambdas to end in end cause it gets too verbose.
and is a lot less pleasing than the other control flow stuff
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.
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)
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.
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?
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:
also, I'm actually not sure it's a bad thing to encourage top-level defs
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
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
so "supported but there's a nudge to do it the other way" doesn't feel like the worst thing to me
has that ever happened?
I definitely have done it for snippets/short lambdas
what was the motivation for bringing it inline?
But I also really like the nested helper lambda structure
do you think that would still be the case if we had for and while?
That said, lambdaset bugs in roc made me mostly stop diong that
e.g. how much of that is specifically for recursion
do you think that would still be the case if we had
forandwhile?
Yeah, probably would mostly go away
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
interestingly, the only places where I did this in roc-realworld wouldn't change their LoC
or at least, wouldn't need to
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 ))
Brendan Hansknecht said:
do you think that would still be the case if we had
forandwhile?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:
I feel like managing base cases almost always is easier with for/while loops.
That said, some list pattern matching recursion is super natural
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
yeah, I can appreciate the "language smallness" benefit of having a smaller set of primitives and not including looping constructs
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
basically I only reach for recursion in cases where if I used a loop I'd build my own stack on the heap anyway
Digging for my own reasoning and came up with only 'it feels cool' haha :sweat_smile: Excited to try looping though
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
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
That's captured by my example syntax, but yes
I'm more thinking of pushing to and popping from a queue
Like for running breadth-first search
how would your syntax deal with multiple patterns? I am talking about many patterns
Okay, yeah, wouldn't deal with it and would need a for loop
I don't think we should get into the specific in this thread, though
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
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.
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
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
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)
yep!
both are unambiguous
I personally prefer the return one just because it has fewer parens :big_smile:
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!(...)
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
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
The when would need an end wouldn't it?
also I think we could easily give nice error messages if you mess this up
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!"
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
Could it be a lambda must be one expression or use return for the last expression? would that be unambiguous?
still ambiguous
can't tell if it's one expression or just a statement followed by more defs etc
Wow this is quite the thread
I don't quite see why parens are necessary in an inner closure?
Anyway, I like this :thumbs_up:
here's an idea for a tutorial snippet explaining the named inner closure thing:
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.
I would add a note about accidentally getting tabbing wrong and how it still just works
But... wait. Isn't it not ambiguous, with the do syntax? I guess that got dropped?
And without do, how is this not also ambiguous at the top level?
do or ; or {} = were _the_ things that got rid of that ambiguity, including at the top level
that's the current proposal
Ahhh so the top level is different from nested layers
I didn't read that carefully enough
I feel like there must be a way to make that rule a bit more consistent
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
Also, I wouldn't call this insignificant-whitespace; it very much is still significant in this proposal (albeit less so)
sure
but copy/pasting just works in practice
add there are no indentation levels you need to get to agree with one another
True
so it feels like it doesn't really have any of the specific drawbacks of significant indentation that we've discussed :big_smile:
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
I'd _really_ like to have something that works uniformly
I would too, but all the other designs we've discussed have had worse drawbacks than this
Slight bump on my idea for having return in top-levels too... I think that's also an option (even if unpopular) :sweat_smile:
I guess I disagree with that assessment :shrug:
which specific design would you prefer?
any of the ones where the rules are consistent between the inner and nested levels :stuck_out_tongue_closed_eyes:
end for all lambdas does that, but I don't think anyone wants to have to write map(|x| x + 1 end)
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)
I think if there's already a clear outer delimiter, we don't need you to put an inner delimiter
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
Oh true, that is weird
yeah I think we need to just rule out do and those variations
Maybe bring back the requirement of a "true" last expression?
e.g. in your case that could be {}
I guess we couldn't really enforce that
yeah Luke suggested return {}
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
Is it a requirement that outer lambdas be delimiter-less (at the end)?
return definitely works just as well as end
it's not a requirement, but giving them delimiters has other downsides
Richard Feldman said:
endfor all lambdas does that, but I don't think anyone wants to have to writemap(|x| x + 1 end)
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
Does return still work for a multiline expression? I think it may still be ambiguous
I like mandating every function return. Now that we have early returns, the last line of a function isn't that special any more.
return is always a sufficient replacement for end because the function can't possibly have anything more to it after it returns
What about, throwback to my days writing Excel macros
main! = |_args|
do Stdout.line("hello, ")
Stdout.line("hello, ")
end main!
but this is too weird imo:
main! = |_args|
Stdout.line("hello, ")
return Stdout.line("hello, ")
also, nobody wants to have to write map(|x| return x + 1) in a functional language :big_smile:
so I think we should rule out "return everywhere" too
I've got my creative hat on
I'm still surprised that {} is becoming simultaneously the most common return type and the most hidden from app developers :laughing:
Are we ashamed of our baby?! Does it need a makeover?
Richard Feldman said:
so I think we should rule out "
returneverywhere" too
I can stop holding my breath
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:
Lol, I just wrote a similar thing...
main! = |_args| (
Stdout.line!("foo");
Stdout.line!("bar");
)
run! = |_| (
...
)
Ooh if we're getting creative/funky with it, here's a probably bad idea: what about bar-wrapping the |return_value| too?
Interesting...
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?
I guess more specifically, |x| x + 1 isn't so much "multi-line" as it is "single expression"
i.e. no value/type defs allowed in that syntax; just a single expression
hm, interesting
main! = |_args| do
adder = (|n| "n + ${Num.to_str(n)}")
Stdout.line!(adder(1));
Stdout.line!(adder(2));
end
run! = |_| do
...
end
so that implies do ... end is not specific to lambdas
it's just another way to write ( ... )
right?
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"
Parens would work equally well in that context, yes
I still like the value of having there be only one syntax for lambdas
So in that sense they are replaceable
plus I think it's sort of implied to be supported haha
If do/end is just sugar for parens, then there kinda is only one lambda syntax
exactly!
Just sometimes you need to "use" it differently, depending on the surrounding context
like if |x| expr is valid and |x| do ... end is valid, then it certainly looks like do ... end is an expr :big_smile:
that also means it can be used for nested defs without parens, which came up earlier
What are the downsides to that approach?
I'm sure this is not the design Niclas would prefer, due to it adding a bunch of do and ends to code bases
it is more consistent, that's for sure
although one-liner lambdas don't change
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));
||
it looks super ambiguous to me :sweat_smile:
I like it more than I expected - 10/10 in symmetry
2/10 in weirdness budget, though very obvious how it works
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
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
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.
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! = |_|
...
I like this more than just requiring end at the end of lambdas because:
map(|x| x + 1)Ironically as the guy who's suggested return for all lambdas... I'm not loving the do end for all lambdas. :sweat_smile:
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
Ahk that makes sense.
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
I think it's maybe because I expect end to be paired with an opening keyword and not a symbol?
I don't see the difference between the last two
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).
Oh "do"
I guess it's pretty unobtrusive! :laughing:
do-end is way better than parens
Honestly if I had to pick one thing, it'd probably be the thing we do currently - i.e. whitespace sensitivity.
Same
do/end would be a close second
Another idea... I don't love it
main! = |_args|
Stdout.line!("hello, world!")
;
is there any possible way we could get the current indentation sensitivity to work with pasting?
or is there any variation on it we could do to improve that situation?
Is it the mixed spaces/tabs thing? Or is this where you paste something that's indented too little into a block?
I'd love to see examples of these paste cases people are tripping on, cause I might be imagining something different.
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:
mixed spaces / tabs is solvable I think
Will need to give some thought to the latter
I think the problem there that I usually run into is copying over a chunk of defs followed by a conditional
and the source was at a different indentation level than the destination
so I have to paste it in and then adjust the indentation level manually until I get it right
Why is the "followed by a conditional" relevant/important?
it might not be, just a common part of the pattern for me
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
I'm just saying... if we want to be a functional language in imperative clothing we should embrace the return :smiley:
we ruled that one out though :big_smile:
Yeah there are definitely cases where you've lost too much info to recover, at least if you only look at the tokens blindly.
I wonder about like...what if indentation was just for lambdas?
and we added end to if and when
I guess that still wouldn't work
That definitely constrains the problem a lot
because what if you paste an outdented thing into a lambda
Sure, but now you solved the pasting-into-when case
true
oh wait
yeah that's actually potentially great
because it would mean pasting would likely only break when pasting into a nested lambda
which would be super rare
We could have the formatter put parens around nested lambdas
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.
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)?))
)
so in that design, nested named lambdas look and work the same as today
and don't need any special explanation
and unless they're named, the nested lambdas will have parens or commas delimiting them anyway due to PNC
@Luke Boswell you mean you've pasted in something over-indented and it accidentally gets slurped into the previous def?
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
What about double newlines \n\n between top-level defs?
So vertical whitespace significance instead of horizontal?
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
(I'm only half joking)
Explain yourself
Or perish
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.
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
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
A really interesting idea, though...
Luke Boswell said:
What about double newlines
\n\nbetween 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.
That's a form of whitespace significance that should be obvious enough to readers to not be as much of an annoyance
Just mash enter until compile good
It feels natural to me. Like how you separate a paragraph in an essay.
Each top-level def is like a new paragraph
So you could have at most one empty line inside a block.
@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?
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!")
}
take my money
this is more concise than do .. end, and { x } can unambiguously mean what it does today: a record
I guess that does mean you're not allowed to (directly) return a record
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 }?
I don't quite follow that part
my claim is that if you take all the valid record expressions today
we can say those still mean what they do today
because you'd never want to use curlies for multiple expressions in a way that could be mistaken for a record
this is not a valid record:
main! = |_args| {
Stdout.line!("hello, world!")
}
and nobody would write this:
main! = |_args| {
x
}
So commas are the difference
and if they wrote this, I think it would obviously be a record:
main! = |_args| { x }
How would if/when/for look in this world?
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
Sam Mohr said:
How would
if/when/forlook in this world?
could be the same as today, or could change
e.g. could just do what Rust does if desired
this is not a valid record:
main! = |_args| {
if foo {
Stdout.line!("hello, world!")
} else {
other
}
}
but it sure does look C-like :joy:
I'm glad Anthony's asleep
It's very consistent
I don't like the record/block confusion
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
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:
like I said, "for the sake of thoroughness" haha
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
obvious the former is more concise, but the latter has a lot of nice properties and simplifications
Dear Lord y'all, this is a long one!
Fwiw in JavaScript foo => ({ bar }) is how you return a record
I'm not sure how bad it is to have end after every single block except for top-level defs
Because that's a consistent rule, and would help remove a lot of these
@Sam Mohr why would top level defs be excluded there?
To make your code, that's it
Or oh you're suggesting an alternative
yes
One that was mentioned prior
That's not consistent with _depth_
i.e. the top level is different from nested levels
Yes
That's what I meant by inconsistent
I don't like that part
I'm using the word differently than you, and yes, I understand
I think using do ... end for multiline blocks makes a lot of sense
that one feels like the simplest, most consistent design that has the desired properties
it's less concise than status quo
If it's only for multiline blocks, does that reopen the door for a multiline-only return?
main! = |_args|
Stdout.line!("hello, world!")
return
right now time I'm imagining all the world's Elixir and Ruby programmers rolling their eyes at the same time :joy:
there is no return door
it has passed the point of no return
more
future return suggestions will be marked return to sender
I don't know how more of these I've got :sweat_smile:
I'm sure it will return later... once we've exhausted all other options...
It will return in your syntax dreams
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?
Luke Boswell said:
It will
returnin your syntax dreams
returrrn the slab block
first could be either status quo, or a variation where we treat either only lambdas or only top-level declarations as having significant indentation
the second one is the idea to have do ... end be equivalent to ( ... )
Jan I love you so much for that reference
and then say that lambda bodies are always 1 expression
but the formatter would choose to add do ... end if you make that one expression multiline
as opposed to map(|x| x + 1)
where it wouldn't add anything to the 1 expression
in that design, named nested lambdas work the same way as top-level ones
there's no indentation sensitivity anywhere
it feels like the simplest and most consistent design, but it has the drawback of being less concise
which I suppose is the general drawback of anything compared to significant indentation, to be fair :big_smile:
Conciseness is sometimes anti-correlated with mental load for human readers!
true, but I don't think so in this case
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
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
I dunno why, just aesthetically
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)
Eleventh hour random idea: done instead of end :smiley:
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
Serious proposal - many would enjoy the symmetry.
Disclaimer: I've never used a lang with end, so this simply reads smoother to me.
Very bash
going back to the "indentation sensitive, but consistent and as error-tolerant as possible" design, what about this?
if, when, for, and while all end in endthis 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
so tab space tab is just 3 characters
And tab isn't "longer" than space?
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
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:
main! = |args| do
doStuff!()
moreStuff!()
end
list.map(|x| x + 1)
if condition do
stuff()
end
when value is
Ok(x) -> do
handleSuccess(x)
cleanup()
end
Err(e) -> handleError(e)
end
Advantages:
Fully consistent rules - blocks are delimited the same way everywhere
No special cases for top-level vs nested declarations
Clear visual structure with matching delimiters
No ambiguity with records unlike using curly braces
More familiar to developers coming from Ruby/Elixir
Copy-paste works reliably since indentation doesn't matter
Parser becomes simpler without whitespace significance
Good error messages possible since block boundaries are explicit
Formatter can enforce consistent style while parser remains flexible
Trade-offs:
More verbose than current whitespace-sensitive syntax
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.
Lol... someone had to try it
well good to know what Claude (?) thinks :laughing:
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
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
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
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
but given how rare those are, it would seem pretty unlikely for anyone to run into it in practice
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
like yeah it's the simplest design, but the practical upside of that simplicity feels very small
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?
I've read this whole topic as "what should the syntax change to in six months" btw
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.
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
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.
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.
That's only possible because Android Studio understands the intended syntax even with incorrect formatting
Mainly because Java has unambiguous syntax with respect to newlines
Current Roc finds it easy to break the syntax such that the code now doesn't know who owns which statements
If the cursor position were taken into account when pasting, that might make something possible?
That would depend on the editor
The compiler doesn't get that info when formatting
Unless we did some diff magic with the LSP?
Pretty sure that's not a thing
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.
I don't really understand what the state of this discussion is :cry:
"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.
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.
I would guess this is the front runner... https://roc.zulipchat.com/#narrow/channel/304641-ideas/topic/insignificant.20whitespace/near/498936924
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?
if,when,for, andwhileall end inend- defs and statements inside named lambdas must begin on a line that's indented at least 1 character more than where the lambda's name begins
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
whenbranch), 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.
It's a compromise, so we've almost got insignificant whitespace... but just enough to keep a sane default and not have end everywhere
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:)
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.
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...
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.
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!).
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.
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.
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:
I split off an idea in #ideas > braces syntax
JanCVanB has marked this topic as resolved.
Last updated: Jun 16 2026 at 16:19 UTC