Stream: ideas

Topic: Remove the need for backslashes for lambdas?


view this post on Zulip Daniel Schierbeck (Dec 12 2024 at 13:13):

This may have already been discussed, but my search didn't show anything.

Now that there's discussion about making Roc look more similar to mainstream languages, I want to bring up something that I've _personally_ found unnecessary in both Elm and now Roc: the backslash before a lambda. Even Java, the verbose language _par excellence_, supports the simple (x) -> x * 2 syntax. JavaScript supports x => x * 2. I see how the whitespace calling option might have made this a bit funky, e.g.

hello x -> x * 2

versus

hello \x -> x * 2

... but with parentheses it's really quite clear:

hello(x -> x * 2)

The backslash seems like a superfluous and confusing piece of syntax to me, since

  1. We use -> _without_ a backslash in function type signatures, and
  2. The -> operator should be sufficient to establish that it's a lambda/function?

view this post on Zulip witoldsz (Dec 12 2024 at 13:56):

Backslash \x -> is still easier than lambda x: in Python, so I wonder why does Python need it?

view this post on Zulip Daniel Schierbeck (Dec 12 2024 at 14:58):

I always wondered that as well, since it's incredibly verbose :laughing:

view this post on Zulip Sam Mohr (Dec 12 2024 at 18:16):

We will probably start the move to parentheses by supporting spaces and parentheses for function calls. We need backslashes to avoid ambiguity in space-gapped function calls, so anonymous functions there will need to keep the backslash, but we should be able to remove the backslashes everywhere else

view this post on Zulip Derin Eryilmaz (Dec 12 2024 at 18:44):

i wonder then if backslash could become a more general thing that just does right associativity

view this post on Zulip Brendan Hansknecht (Dec 12 2024 at 19:37):

The paren removal operator?

view this post on Zulip Richard Feldman (Dec 12 2024 at 22:11):

I think that idea should be a separate thread :big_smile:

view this post on Zulip Richard Feldman (Dec 13 2024 at 03:42):

@Daniel Schierbeck what would this look like in more scenarios? for example:

it would be ideal to see these scenarios side by side with status quo in the parens-and-commas syntax, to get a better idea of how they compare :big_smile:

view this post on Zulip Daniel Schierbeck (Dec 13 2024 at 09:27):

@Richard Feldman OK, tried to write up a somewhat comprehensive list of examples: https://gist.github.com/dasch/b11324ec50a5de9e3e460996b795b6e0

view this post on Zulip Daniel Schierbeck (Dec 13 2024 at 09:31):

I think the most tricky example was functions as record values, e.g. { update: \state, input -> foo state input }. However, that does point to something I _also_ find really weird about Roc, which is the asymmetry between how functions have been defined vs called – and I guess that's what the parens proposal also addresses? If so, wouldn't it make more sense to use { update: (state, input) -> foo state input }. Syntactically and conceptually, the arguments are being passed in as a tuple. If there's no direct support for currying, isn't that what's happening anyway? So backslash-less functions can be defined as e.g. x -> ... for single-argument functions, {} -> ... for zero-argument functions, or (x, y) -> ... for multi-argument functions. This seems by far the most readable to me.

view this post on Zulip Anton (Dec 13 2024 at 10:56):

Richard Feldman OK, tried to write up a somewhat comprehensive list of examples: https://gist.github.com/dasch/b11324ec50a5de9e3e460996b795b6e0

Most of those look good, in cases like below, I do have a strong preference for the backslash:

# current
people
|> List.map \p -> p.age

# proposed
people
|> List.map p -> p.age

it's really nice to have a clear boundary for the start of the function

view this post on Zulip Alex Nuttall (Dec 13 2024 at 11:01):

Daniel Schierbeck said:

wouldn't it make more sense to use { update: (state, input) -> foo state input }.

I think parentheses around parameters could also mitigate the need to put parentheses around whole lambdas in bracket-calling discussed here. It's kind of the javascript solution (though brackets can be dropped for single parameters there)

view this post on Zulip Sam Mohr (Dec 13 2024 at 11:01):

@Anton how much is this important to you for parens-based function calling? It seems like your problem is with removing the backslash for space-based function calls, which we can simply avoid by only enabling this for parens-based calls

view this post on Zulip Sam Mohr (Dec 13 2024 at 11:03):

As much as parens around the args seems to help with ambiguity, I think it is a problem when used with tuples. JS doesn't have tuples so it's not a problem for them, but we do

view this post on Zulip Alex Nuttall (Dec 13 2024 at 11:04):

gleam uses #() :man_shrugging:

view this post on Zulip Anton (Dec 13 2024 at 11:04):

@Anton how much is this important to you for parens-based function calling?

With parens it could indeed be a tuple argument or a start of a function, I don't like the added thinking

view this post on Zulip Sam Mohr (Dec 13 2024 at 11:11):

I don't think it is added thinking?

List.mapIndexed(item, index as i -> List.repeat(item, i))

view this post on Zulip Sam Mohr (Dec 13 2024 at 11:12):

Let's ignore space-delimited argument calling syntax, as it would go away in static dispatch land (unless you really think we shouldn't ignore it)

view this post on Zulip Sam Mohr (Dec 13 2024 at 11:14):

With just parens calling world, if we don't surround the argument list with parens, then you just need to look for the arrow

view this post on Zulip Sam Mohr (Dec 13 2024 at 11:14):

It's not quite as clear as backslash I agree, but it's cleaner

view this post on Zulip Sam Mohr (Dec 13 2024 at 11:15):

The benefit of backslash is that it uniquely defines the start of a function def

view this post on Zulip Sam Mohr (Dec 13 2024 at 11:16):

But if it isn't necessary, it's just an extra character to type (ignoring our historical bias of having used it in Roc for a few years)

view this post on Zulip Alex Nuttall (Dec 13 2024 at 11:16):

Just to rehash my example from the other thread one more time, here it is with bracket params (and :(a, b) for tuples):

parse = (str) ->
    str
    .to_utf8
    .split_on('\n')
    .walk_with_index(Dict.empty(), (dict, row, y) ->
        row.walk_with_index(dict, (row_dict, cell, x) ->
            row_dict.insert(:(x.to_i16(), y.to_i16()), cell - '0'))
        )
    )

view this post on Zulip Sam Mohr (Dec 13 2024 at 11:16):

nitpick: Dict.empty {} -> Dict.empty()

view this post on Zulip Sam Mohr (Dec 13 2024 at 11:17):

This example isn't great because it has a lot of callbacks, but I think that's not the fault of the example

view this post on Zulip Sam Mohr (Dec 13 2024 at 11:17):

Otherwise, I think this is very readable

view this post on Zulip Alex Nuttall (Dec 13 2024 at 11:18):

I think the bracket params help precisely with the readability of callbacks though. Otherwise you have this:

parse = \str ->
    str
    .to_utf8
    .split_on('\n')
    .walk_with_index(Dict.empty(), (\dict, row, y ->
        row.walk_with_index(dict, (\row_dict, cell, x ->
            row_dict.insert((x.to_i16(), y.to_i16()), cell - '0'))
        ))
    ))

view this post on Zulip Alex Nuttall (Dec 13 2024 at 11:18):

or this:

parse = \str ->
    str.to_utf8()
    .split_on('\n')
    .walk_with_index(
        Dict.empty(),
        (\dict, row, y ->
            row.walk_with_index(
                dict,
                (\row_dict, cell, x ->
                    row_dict.insert((Num.toI16(x), Num.toI16(y)), cell - '0')
                )
            )
        )
    )

view this post on Zulip Sam Mohr (Dec 13 2024 at 11:24):

That first example I think has redundant parentheses for what would be necessary for parsing:

parse = \str ->
    str
    .to_utf8
    .split_on('\n')
    .walk_with_index(Dict.empty(), \dict, row, y ->
        row.walk_with_index(dict, \row_dict, cell, x ->
            row_dict.insert((Num.to_i16 x, Num.to_i16 y), cell - '0')
        )
    )

view this post on Zulip Sam Mohr (Dec 13 2024 at 11:26):

And sans redundant parens, it's pretty clear to me where the callbacks are (given that this Elm syntax highlighting makes the backslashes pretty obvious)

view this post on Zulip Alex Nuttall (Dec 13 2024 at 11:30):

Seems reasonable, but opinions differed on the other thread about whether they should be required (even if not technically required)

view this post on Zulip Sam Mohr (Dec 13 2024 at 11:42):

Which thread?

view this post on Zulip Alex Nuttall (Dec 13 2024 at 11:44):

Was actually only thinking of @Brendan Hansknecht's response here, and he specifically talks about allowing that case anyway

view this post on Zulip Anthony Bullard (Dec 13 2024 at 11:45):

Also paren grouped params to start a function in most positions require backtracking to ensure it's not just a tuple

view this post on Zulip Daniel Schierbeck (Dec 13 2024 at 11:47):

Would tuple arguments actually be a problem? We could require that in order to take in and deconstruct a tuple you need to wrap it in another layer of parens, e.g. ((x, y)) -> dist(x, y) or something; but when would you not just turn that into a function with 2 args, or if you really need to pass tuples, to do coords -> (x, y) = coords; dist(x, y)?

view this post on Zulip Anthony Bullard (Dec 13 2024 at 12:31):

It's not for if the tuple is argument being destructured mainly (though that's also maybe a worse problem), but you have to first look for a lambda and if you don't find the -> directly after the parenthesized statement, backtrack and then look for a tuple

view this post on Zulip Alex Nuttall (Dec 13 2024 at 12:35):

I think a different tuple syntax would be a plus if there were bracket params:

view this post on Zulip Anton (Dec 13 2024 at 13:21):

A tuple operator like that seems very rare among popular programming languages, that makes me hesitant

view this post on Zulip Norbert Hajagos (Dec 13 2024 at 15:44):

I don't think swapping the backslash for a pair of parens is a plus. There is no need for the closing parens from a parsing point, since the end of the argument list is marked with ->. When I see a backslash, I know that's the start of a lambda. Seeing a ( doesn't tell me what is beginning there, i would have to look for the -> to confirm my suspicion. A different tuple operator would be strange, more so than the \ for lambdas.

view this post on Zulip Norbert Hajagos (Dec 13 2024 at 15:55):

Plus if we are adding more parens for function calling with static dispatch, I don't think the right syntax is to introduce even more. Maybe a different char, like Rust (Smalltalk :wink: ) styled blocks. I also remember a general positive feedback in one of the zulip discussion to swap || to or, which would free up the pipe char. Not advocating for them though, I love our current lambda syntax.

to_rows = \input, delim ->
    List.split_on(input, delim)
to_rows = (input, delim) ->
    List.split_on(input, delim)
to_rows = |input, delim|
    List.split_on(input, delim)

Edit: converted to snake_case. Lots of future syntax change to keep in my head.

view this post on Zulip Brendan Hansknecht (Dec 13 2024 at 16:17):

A general note: I think it is a bad idea to make it so we are passing tuples to functions by default. It has significant meaning to layout and thus performance.

So with parens around the args syntax, I think this would be required to pass a tuple as:

fn = ((field1, field2)) ->
     ...

view this post on Zulip Brendan Hansknecht (Dec 13 2024 at 16:18):

Or, as mentioned above, pick a different tuple syntax.

view this post on Zulip Brendan Hansknecht (Dec 13 2024 at 16:19):

But I think also as mentioned above, it is probably less surprising to pick a different function syntax

view this post on Zulip Richard Feldman (Dec 13 2024 at 22:18):

I've been sick all day, so I haven't caught up on the whole thread, but I thought the original idea was just:

view this post on Zulip Richard Feldman (Dec 13 2024 at 22:19):

tl;dr I thought the idea was "in the parens-and-commas and methods world, delete \ and change nothing else" and that does seem like a reasonable idea to me, all things considered :big_smile:

view this post on Zulip Brendan Hansknecht (Dec 13 2024 at 22:28):

What would list.walk look like? This?

my_list.walk(init_state, (state, elem -> ...))

view this post on Zulip Richard Feldman (Dec 13 2024 at 22:49):

yeah

view this post on Zulip Richard Feldman (Dec 13 2024 at 22:50):

that's probably the most common situation where you'd need an extra character, although I think in the world where we have for, walk would be less commonly used

view this post on Zulip Brendan Hansknecht (Dec 13 2024 at 22:53):

I personally dislike it as a gut reaction (mostly just feels ambiguous), but I also understand that in the parens world, parens will be required more often around lambdas. So it makes senses to just embrace that.

view this post on Zulip Alex Nuttall (Dec 13 2024 at 23:03):

I think the downside of brackets around lambdas is not really about an extra character - it's an extra character which needs to stay matched with another bracket a few lines away. Editing whitespace code is much nicer because it doesn't require that bracket admin.

There are many standard library functions that use callbacks beyond walk. Things that couldn't be replaced by for loops, like findFirst. I don't image that for loops would replace map and similar functions either (though they could).

I'm still hoping that they don't end up being required, somehow

view this post on Zulip Brendan Hansknecht (Dec 13 2024 at 23:07):

In the list builtins, aside from loops, only update and map2 through map4 are effected by dropping the \

view this post on Zulip Brendan Hansknecht (Dec 13 2024 at 23:08):

The others have the lambda in the second arg, so it wouldn't need extra parents in method call syntax.

view this post on Zulip Brendan Hansknecht (Dec 13 2024 at 23:09):

my_list.update(7, (a -> a + 1))

view this post on Zulip Brendan Hansknecht (Dec 13 2024 at 23:10):

Alex Nuttall said:

I think the downside of brackets around lambdas is not really about an extra character - it's an extra character which needs to stay matched with another bracket a few lines away. Editing whitespace code is much nicer because it doesn't require that bracket admin.

For context, roc is already planning to test out a parens and comma syntax instead of a white space calling syntax. This is a sub proposal of that.

view this post on Zulip Alex Nuttall (Dec 13 2024 at 23:14):

Brendan Hansknecht said:

Alex Nuttall said:

I think the downside of brackets around lambdas is not really about an extra character - it's an extra character which needs to stay matched with another bracket a few lines away. Editing whitespace code is much nicer because it doesn't require that bracket admin.

For context, roc is already planning to test out a parens and comma syntax instead of a white space calling syntax. This is a sub proposal of that.

Yes, I know. I'm just thinking: what would Javascript look like with brackets surrounding every arrow function? - and that's a lot of brackets.

view this post on Zulip Sam Mohr (Dec 13 2024 at 23:14):

We probably don't need to opt for this either, but Kotlin allows for last-arg callbacks to be provided outside of the parentheses. Kotlin's are easier to parse because they use { and } for functions, but it should be unambiguous if we just require that the closing ) function call parens and the arg list of the function def are on the same line.

my_list.update(7) a ->
    a + 1

my_list.update(7) a -> a + 1

parse = str ->
    str
    .to_utf8
    .split_on('\n')
    .walk_with_index(Dict.empty()) dict, row, y ->
        row.walk_with_index(dict) row_dict, cell, x ->
            row_dict.insert((Num.to_i16 x, Num.to_i16 y), cell - '0')

If we're worried about too many nested parentheses making reading harder

view this post on Zulip Anthony Bullard (Dec 13 2024 at 23:15):

Alex Nuttall said:

Yes, I know. I'm just thinking: what would Javascript look like with brackets surrounding every arrow function? - and that's a lot of brackets.

Like Javascript before arrow functions :smile:

view this post on Zulip Brendan Hansknecht (Dec 13 2024 at 23:16):

Yes, I know. I'm just thinking: what would Javascript look like with brackets surrounding every arrow function? - and that's a lot of brackets.

I think most cases no brackets would be needed cause there is no ambiguity.

view this post on Zulip Alex Nuttall (Dec 13 2024 at 23:20):

Yes that's right, there are many less standard library functions requiring it than I thought

view this post on Zulip Richard Feldman (Dec 14 2024 at 00:12):

:thinking: is this already the exact syntax we use for functions in type annotations?

view this post on Zulip Brendan Hansknecht (Dec 14 2024 at 00:31):

Yes

view this post on Zulip Brendan Hansknecht (Dec 14 2024 at 00:33):

Caveat being they often appear filling in a type variable in a single set of parens vs being repeated many times in the literal

view this post on Zulip Brendan Hansknecht (Dec 14 2024 at 00:34):

Eg List (a -> a) vs:

[ (a -> a + 1), (a -> a - 1) ]

view this post on Zulip Daniel Schierbeck (Dec 14 2024 at 13:32):

If the point of the dots-and-parens syntax is to seem more familiar to newcomers, wouldn’t it make sense to lean on e.g. the JavaScript syntax rather than something bespoke? That would be x -> … and (x, y) -> …, instantly recognizable to the very large group of people who know JS. I think the commas-but-no-parens style is kind of the worst option, really – different from how the function will be _called_, so not symmetrical, but also not easy for newcomers to understand. I think the Elm style of x -> y -> … makes a ton of sense when the language supports currying, and has the added benefit that there’s only ever one “argument” for a function, but since currying is out of the picture, why not take the step fully towards the JS syntax?

view this post on Zulip lue (Dec 14 2024 at 13:39):

btw elm doesn't use a -> b -> result for lambdas. It uses \a b -> result (function types do use A -> B -> Result tho).
Do you know how js handles tuple lambda arguments? Oh wait, I guess it doesn't have them, just arrays

view this post on Zulip Daniel Schierbeck (Dec 14 2024 at 13:50):

Ah yes, sorry, mixed up the type signature vs call, which also confused me (it’s been years since I wrote anything in Elm)

view this post on Zulip Richard Feldman (Dec 14 2024 at 14:02):

Daniel Schierbeck said:

wouldn’t it make sense to lean on e.g. the JavaScript syntax rather than something bespoke? That would be x -> … and (x, y) -> …, instantly recognizable to the very large group of people who know JS.

JavaScript doesn't have tuples. If that were the syntax, x -> ... and (x, y) -> ... could both be describing 1-arg functions.

view this post on Zulip Richard Feldman (Dec 14 2024 at 14:04):

Daniel Schierbeck said:

I think the commas-but-no-parens style is kind of the worst option, really – different from how the function will be _called_, so not symmetrical, but also not easy for newcomers to understand.

I think the idea that x -> ... and (x, y) -> ... is easy for newcomers to understand, but x -> ... and x, y -> ... is "not easy for newcomers to understand" is a bit of a stretch :wink:

view this post on Zulip jan kili (Dec 14 2024 at 14:15):

Would anything above be simpler if tuple syntax changed to use {} to match records?

From the tutorial:

Visually, tuples in Roc look like lists (but with parentheses instead of square brackets). However, a tuple is much more like a record - in fact, tuples and records compile down to the exact same representation at runtime! So anywhere you would use a tuple, you can use a record instead, and the in-memory representation will be exactly the same.
Like records, tuples are fixed-length and can't be iterated over. Also, they can contain values of different types. The difference is that in a record, each field is labeled (and their position doesn't matter), whereas in a tuple, each field is specified by its position.

If we're introducing 2-10x parentheses into Roc apps, dissolving tuple syntax to become unlabeled records seems like it could be helpful. I also notice that {} currently means "zero-arg function input"/"empty function output", which feels just as related to an "empty tuple" as an "empty record".

view this post on Zulip Richard Feldman (Dec 14 2024 at 14:16):

{ x, y } already means something else in Roc

view this post on Zulip jan kili (Dec 14 2024 at 14:19):

But does it REALLY though? If that's a shorthand for using local variable names to label the fields, then the distinction is just whether the backing type is labeled vs. positional. If the type is named, the choice is referenceable. If the type is anonymous, then Roc can have a preference for which choice it infers.

view this post on Zulip jan kili (Dec 14 2024 at 14:43):

and { 10, "gallon", Hat } is currently unused space in syntaxland

view this post on Zulip jan kili (Dec 14 2024 at 14:46):

Idk if we'd go so far as to say "If you're looking for tuples, Roc has positional records." but we could, if that would simplify the language overall.

view this post on Zulip jan kili (Dec 14 2024 at 14:48):

This may be a separate thread, but many comments above seem to hit on conflicts between tuple parens and non-tuple parens.

view this post on Zulip Richard Feldman (Dec 14 2024 at 16:12):

I dunno, I personally wouldn't call it a simplification to the language if looking at { x, y } = in isolation, it was no longer possible to know whether that was destructuring a record or a tuple :sweat_smile:

view this post on Zulip Anthony Bullard (Dec 14 2024 at 17:43):

I agree with Richard, and field punning is just too damn useful to lose(and the only viable option). And plus “positional records” would look just like a list but with squirlies :grinning_face_with_smiling_eyes:.

How come no one talks about using <> for tuples?

ducks

view this post on Zulip Notification Bot (Dec 14 2024 at 18:17):

42 messages were moved from this topic to #ideas > Do we need tuples? by Brendan Hansknecht.

view this post on Zulip Sky Rose (Dec 17 2024 at 16:53):

Prettier (javascript formatter) formats its arrow functions with parens around the parameters, even if there's only one parameter, like (x) => x + 1.

At first glance, avoiding parentheses may look like a better choice because of less visual noise. However, when Prettier removes parentheses, it becomes harder to add type annotations, extra arguments or default values as well as making other changes. Consistent use of parentheses provides a better developer experience when editing real codebases, which justifies the default value for the option. (docs).

Assuming we resolve the tuple ambiguity (#ideas > Change tuple syntax from () to {}? ) I'd propose doing the same, though I haven't thought through it all the way.

view this post on Zulip Brendan Hansknecht (Dec 17 2024 at 17:16):

I assume that is needed due to inline types

view this post on Zulip Brendan Hansknecht (Dec 17 2024 at 17:16):

I don't think it applies to roc due to types being on a different line

view this post on Zulip Brendan Hansknecht (Dec 17 2024 at 17:17):

We also don't have any form of named args

view this post on Zulip Brendan Hansknecht (Dec 17 2024 at 17:17):

Or default values

view this post on Zulip Brendan Hansknecht (Dec 17 2024 at 17:17):

All of that is done through records

view this post on Zulip Richard Feldman (Dec 17 2024 at 17:49):

I've seen the suggestion of parens around function args a few times. Here's an example with backslash, without backslash, and without backslash but with parens added:

as discussed previously, the extra delimiter here will almost always be redundant, except in the specific case of there being a comma right before the function begins, which should be rare in the methods world

view this post on Zulip Richard Feldman (Dec 17 2024 at 17:50):

so is the main pitch here "JavaScript and Java have this redundant clutter, so maybe we should too?"

view this post on Zulip Richard Feldman (Dec 17 2024 at 17:50):

or am I missing something? :sweat_smile:

view this post on Zulip Richard Feldman (Dec 17 2024 at 17:52):

or is the concern that dict.map(key, value -> key + value) looks confusing because it's unclear whether key, goes to the lambda vs. being the first argument to .map?

view this post on Zulip Anthony Bullard (Dec 17 2024 at 17:56):

Richard Feldman said:

so is the main pitch here "JavaScript and Java have this redundant clutter, so maybe we should too?"

To be fair, isn’t that the argument for parens and commas in general?

view this post on Zulip Anthony Bullard (Dec 17 2024 at 17:57):

Can we make backslash free work with a performant parser? That seems like a lot of necessary backtracking

view this post on Zulip Sam Mohr (Dec 17 2024 at 17:57):

I'd say parens and commans are better for an IDE-driven experience because they let you discover APIs as you type using dot-based autocomplete

view this post on Zulip Sam Mohr (Dec 17 2024 at 17:58):

And they happen to be less scary to newcomers

view this post on Zulip Sam Mohr (Dec 17 2024 at 17:58):

As an added benefit, they are also easier to parse

view this post on Zulip Anthony Bullard (Dec 17 2024 at 17:58):

I’d say parens and commas are only slightly related to better IDE experience

view this post on Zulip Anthony Bullard (Dec 17 2024 at 17:58):

(deleted)

view this post on Zulip Anthony Bullard (Dec 17 2024 at 17:58):

That’s static dispatch

view this post on Zulip Anthony Bullard (Dec 17 2024 at 17:59):

Though yes, parsing is much easier

view this post on Zulip Richard Feldman (Dec 17 2024 at 17:59):

Anthony Bullard said:

Richard Feldman said:

so is the main pitch here "JavaScript and Java have this redundant clutter, so maybe we should too?"

To be fair, isn’t that the argument for parens and commas in general?

I think the main argument for parens and commas is that they are a prerequisite for dots and static dispatch, and that's where the benefits come

view this post on Zulip Anthony Bullard (Dec 17 2024 at 18:00):

I think you can do static dispatch without parens and commas (can we say PNC?)

view this post on Zulip Sam Mohr (Dec 17 2024 at 18:01):

"Stop trying to make fetch happen!"

view this post on Zulip Sam Mohr (Dec 17 2024 at 18:01):

No but actually, I'm okay with PNC or something

view this post on Zulip Sam Mohr (Dec 17 2024 at 18:01):

"parens-based function calls" has been a lot to type every time

view this post on Zulip Anthony Bullard (Dec 17 2024 at 18:01):

It’s just a lot to type. Like parens and commas :drum:

view this post on Zulip Anthony Bullard (Dec 17 2024 at 18:04):

(deleted)

view this post on Zulip Anthony Bullard (Dec 17 2024 at 18:04):

But we can do

list = [1,2,3]
list->map \n -> n +1

Or

list = [1,2,3]
list:map \n -> n +1

And it works the same

view this post on Zulip Richard Feldman (Dec 17 2024 at 18:04):

Anthony Bullard said:

Can we make backslash free work with a performant parser? That seems like a lot of necessary backtracking

should be doable without backtracking. It's the same problem as { foo, bar, baz } looking like a record until you hit = and then it's like "oh that was a pattern, I'll just store it as a pattern instead of an expr" - you don't have to re-parse, you just change how you're interpreting the data you just parsed

view this post on Zulip Anthony Bullard (Dec 17 2024 at 18:06):

Ok, I guess I’m used to making more strictly recursive descent parsers. You’d have trouble normally saying “this parser will give you a Expr or a Pattern”. But I’m sure you just continue parsing until you get one?

view this post on Zulip Anthony Bullard (Dec 17 2024 at 18:07):

Anthony Bullard said:

But we can do

list = [1,2,3]
list->map \n -> n +1

Or

list = [1,2,3]
list:map \n -> n +1

And it works the same

Anyway I think something like this is a way we could test PNC and SD separately

view this post on Zulip Richard Feldman (Dec 17 2024 at 18:12):

Anthony Bullard said:

Ok, I guess I’m used to making more strictly recursive descent parsers. You’d have trouble normally saying “this parser will give you a Expr or a Pattern”. But I’m sure you just continue parsing until you get one?

I think what we do right now is parse the one and then convert it to the other if we realize that's the situation we're in (e.g. convert an Expr to a Pattern or vice versa) but I think in a future version of the parser we could potentially be more clever and have a combined data structure that's essentially both, and then only canonicalization has a distinction between exprs and patterns based on what it's currently doing

view this post on Zulip Richard Feldman (Dec 17 2024 at 18:12):

e.g. the parser accepts x = foo as bar even though as can only appear in patterns, but then canonicalization encounters it and reports an error because as occurred in an expr position

view this post on Zulip Richard Feldman (Dec 17 2024 at 18:13):

makes the parser more fault-tolerant too, which helps the formatter

view this post on Zulip Sam Mohr (Dec 17 2024 at 18:14):

I actually was gonna propose we consolidate all parsing for records (types, literals, module params, etc.) to a single parser as well at some point. It makes us more tolerant and do less backtracking to parse in this way, and makes the parsing code easier to understand

view this post on Zulip Daniel Schierbeck (Dec 17 2024 at 21:41):

Richard Feldman said:

or is the concern that dict.map(key, value -> key + value) looks confusing because it's unclear whether key, goes to the lambda vs. being the first argument to .map?

I for one would read that as dict.map(key, (value -> key + value)), but maybe that’s because I’m used to these other languages where that’s definitely how it would work.

view this post on Zulip Daniel Schierbeck (Dec 17 2024 at 21:43):

But even dict.map(\key, value -> key + value) would require me to pay extra attention, since I’m used to the comma in a parenthesized argument list being the argument separator. I mean, Roc can be its own thing, but if recognizable syntax is a goal or strategy, then I think the backslash would be a source of confusion.

view this post on Zulip Richard Feldman (Dec 17 2024 at 21:47):

yeah both good points

view this post on Zulip Joshua Warner (Dec 18 2024 at 00:06):

I would also note that parsing comma-separated args in backpassing is quite painful right now, and I was really hoping to sunset that logic with backpassing.

x, y -> foo is annoying to parse for the same reasons as x, y <- foo

I would strongly prefer some solution that doesn't leave the comma "naked". That could be either having some introducing character/sequence (replacing \), or requiring that the arg list be delimited by ()/[]/{}/whatever.

view this post on Zulip Joshua Warner (Dec 18 2024 at 00:06):

(I have no objection to single-arg functions being just x -> foo tho)

view this post on Zulip Richard Feldman (Dec 18 2024 at 00:52):

also a fair point

view this post on Zulip Richard Feldman (Dec 18 2024 at 00:55):

I wonder if this is why Rust ended up on | for lambdas :thinking:

view this post on Zulip Sam Mohr (Dec 18 2024 at 00:55):

I'm gonna be trying to resuscitate that |lambda| thread later for that reason.

view this post on Zulip Richard Feldman (Dec 18 2024 at 00:56):

just as concise as -> but without the ambiguity concern, and already mainstream from Ruby

view this post on Zulip Sam Mohr (Dec 18 2024 at 00:56):

I think | for lambda's helps with readability, it's easy to type, and it literally looks nice

view this post on Zulip Richard Feldman (Dec 18 2024 at 00:56):

the considerations are definitely worth revisiting in the context of parens-and-commas

view this post on Zulip Sam Mohr (Dec 18 2024 at 00:57):

And it keeps the door open to Kotlin-style outside-of-parens final callback args, which are pretty good for DSLs in addition to helping remove some layers of circle brackets

view this post on Zulip Sam Mohr (Dec 18 2024 at 00:58):

Though I don't know how much other people like that behavior

view this post on Zulip Richard Feldman (Dec 18 2024 at 00:58):

it's always felt conceptually weird to me that || in Rust also means 0-arg lambda, but somehow it's never confusing in practice

view this post on Zulip Richard Feldman (Dec 18 2024 at 00:59):

like it feels like it ought to be but somehow it isn't, which I find very strange :big_smile:

view this post on Zulip Richard Feldman (Dec 18 2024 at 01:00):

I know it's unambiguous because it's only an infix operator when it's in the infix position, but still...

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 01:00):

Besides the issue with tab based indentation and clarity, I love the rest of |arg| for lambdas

view this post on Zulip Richard Feldman (Dec 18 2024 at 01:01):

oh what's the issue with tab based indentation and clarity?

view this post on Zulip Anthony Bullard (Dec 18 2024 at 01:01):

We should keep a little m collection of the same module with all of these syntactic combinations

view this post on Zulip Anthony Bullard (Dec 18 2024 at 01:02):

Richard Feldman said:

oh what's the issue with tab based indentation and clarity?

I was about to ask the same

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 01:12):

More context directly above this message in the old thread:
Brendan Hansknecht said:

eg these two look too similar but mean very different things:

credits = List.map songs |song|
    "Performed by $(song.artist)"

credits = List.map songs (song)
    "Performed by $(song.artist)"

view this post on Zulip Richard Feldman (Dec 18 2024 at 01:13):

ah right

view this post on Zulip Richard Feldman (Dec 18 2024 at 01:14):

credits = songs.map(\song -> "Performed by $(song.artist)")
credits = songs.map(|song| "Performed by $(song.artist)")

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 01:14):

Oh, with parens it probably is fine

view this post on Zulip Richard Feldman (Dec 18 2024 at 01:15):

yeah the whole discussion only makes sense in the context of parens haha

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 01:15):

So sounds good to use ||

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 01:15):

Yeah, I know the discussions are about parens, but my brain hasn't rewired to parens yet.

view this post on Zulip Richard Feldman (Dec 18 2024 at 01:16):

{ first, second, third } = { Result.parallel! <-
    first: || do_something!(),
    second: || do_something_else!(),
    third: || do_another_thing!(),
}()?

view this post on Zulip Richard Feldman (Dec 18 2024 at 01:17):

could work! :thumbs_up:

view this post on Zulip Anthony Bullard (Dec 18 2024 at 01:43):

One weird question: Does PNC and this change mean anything for:

  1. Type annotations?
  2. Module params
  3. (Related to above) import params?

view this post on Zulip Anthony Bullard (Dec 18 2024 at 01:46):

map : | List(a), (| a | b) | List(b)

:thinking:

view this post on Zulip Anthony Bullard (Dec 18 2024 at 01:46):

Ok I hate it, plz no

view this post on Zulip Anthony Bullard (Dec 18 2024 at 01:46):

Kill number 1

view this post on Zulip Luke Boswell (Dec 18 2024 at 01:47):

What did I just read.... :see_no_evil:

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 01:48):

Oh, also, if we remove ->, what comes of =>?

view this post on Zulip Sam Mohr (Dec 18 2024 at 01:49):

I think type annotations could be more parenthetical with respect to type constructors. AKA complex types and tags:

Tag ext : [Left, Right(Str, U64), ..ext]

Container elem : { items : List(elem) }

view this post on Zulip Sam Mohr (Dec 18 2024 at 01:50):

Left could be Left(), Tag ext could be Tag(ext)

view this post on Zulip Sam Mohr (Dec 18 2024 at 01:50):

Or we go Rust/C++ and do Tag<ext> or Right<Str, U64>

view this post on Zulip Sam Mohr (Dec 18 2024 at 01:51):

The thing we agreed on is the ..ext allowing for spread-style extensions

view this post on Zulip Luke Boswell (Dec 18 2024 at 01:52):

It feels like we're heading in the direction of future Roc; the :wolf: Elm in :sheep: Rust's clothing

view this post on Zulip Sam Mohr (Dec 18 2024 at 01:52):

Brendan Hansknecht said:

Oh, also, if we remove ->, what comes of =>?

This is the main reason we still need the arrow, and I don't even think that we'd be better of without the arrow. It's extremely obvious what it means

view this post on Zulip Sam Mohr (Dec 18 2024 at 01:52):

Luke Boswell said:

It feels like we're heading in the direction of future Roc; the :wolf: Elm in :sheep: Rust's clothing

Literally what Gleam did

view this post on Zulip Anthony Bullard (Dec 18 2024 at 01:52):

I leave this language the second <> are introduced

view this post on Zulip Sam Mohr (Dec 18 2024 at 01:52):

Okay, line drawn haha

view this post on Zulip Luke Boswell (Dec 18 2024 at 01:52):

Could be a good way to shed the non-believers

view this post on Zulip Sam Mohr (Dec 18 2024 at 01:52):

I don't have strong opinions on them one way or another. Any particular reason @Anthony Bullard ?

view this post on Zulip Luke Boswell (Dec 18 2024 at 01:53):

Keep the tyre kickers away

view this post on Zulip Anthony Bullard (Dec 18 2024 at 01:53):

I just hate them aesthetically

view this post on Zulip Sam Mohr (Dec 18 2024 at 01:53):

Fair

view this post on Zulip Anthony Bullard (Dec 18 2024 at 01:59):

I think Roc is beautiful right now

view this post on Zulip Anthony Bullard (Dec 18 2024 at 02:00):

That’s why I keep trying to figure out how we could do static dispatch without PNC

view this post on Zulip Richard Feldman (Dec 18 2024 at 02:32):

Brendan Hansknecht said:

Oh, also, if we remove ->, what comes of =>?

I didn't even think of this, but actually | for delimiting functions instead of -> actually resolves the longstanding irritation of => functions' bodies being defined with -> syntax

view this post on Zulip Richard Feldman (Dec 18 2024 at 02:33):

main! = |args| ...

view this post on Zulip Richard Feldman (Dec 18 2024 at 02:33):

so I guess basic-cli Hello World would be:

main! = |_args|
    Stdout.line!("Hello, World!")

view this post on Zulip Richard Feldman (Dec 18 2024 at 02:34):

or, if preferred:

main! = |_|
    Stdout.line!("Hello, World!")

view this post on Zulip Richard Feldman (Dec 18 2024 at 02:34):

I don't know if there's a canonical name for this in Rust, but I propose that |_| be referred to as "touchdown!" :american_football:

view this post on Zulip Richard Feldman (Dec 18 2024 at 02:35):

if we didn't have it passing in args, it would be:

main! = ||
    Stdout.line!("Hello, World!")

view this post on Zulip Sam Mohr (Dec 18 2024 at 02:40):

It's very elegant.

view this post on Zulip Anthony Bullard (Dec 18 2024 at 02:48):

Richard Feldman said:

or, if preferred:

main! = |_|
    Stdout.line!("Hello, World!")

Love it

view this post on Zulip Luke Boswell (Dec 18 2024 at 02:48):

Kind of reminds me of the eye of sauron.

view this post on Zulip Anthony Bullard (Dec 18 2024 at 02:51):

Anthony Bullard said:

We should keep a little m collection of the same module with all of these syntactic combinations

Thinking about this. Would it be costly to maintain a fixed (no new features, not meant for anything than formatting) version of the compiler that just has a configurable formatter for seeing projects in different proposed syntaxes? It would only parse Roc that was valid at the time the compiler was forked.

view this post on Zulip Sam Mohr (Dec 18 2024 at 02:52):

Any reason this couldn't just be https://github.com/gamebox/roc?

view this post on Zulip Anthony Bullard (Dec 18 2024 at 02:52):

Sam Mohr said:

Any reason this couldn't just be https://github.com/gamebox/roc?

You mean I branch I maintain? Sure

view this post on Zulip Luke Boswell (Dec 18 2024 at 02:53):

Maybe hold this thought... I think Josh has been cooking

view this post on Zulip Luke Boswell (Dec 18 2024 at 02:54):

We've got a basic platform with the idea to provide the AST https://github.com/lukewilliamboswell/roc-platform-template-rust/tree/osprey-poc

view this post on Zulip Luke Boswell (Dec 18 2024 at 02:55):

Just an idea at this stage.

But it would be cool to have something like that parse a roc module, and then format it using our alternative syntaxes

view this post on Zulip Sam Mohr (Dec 18 2024 at 02:56):

Another long-term goal of this team is to make the roc compiler into a set of exposed libraries. That's nowhere near-term, but something that could help support this kind of goal in the future.

view this post on Zulip Luke Boswell (Dec 18 2024 at 02:59):

If your ok working with rust, you can use libroc today :grinning: aka the above platform concept.

view this post on Zulip jan kili (Dec 18 2024 at 14:57):

Richard Feldman said:

Brendan Hansknecht said:

Oh, also, if we remove ->, what comes of =>?

I didn't even think of this, but actually | for delimiting functions instead of -> actually resolves the longstanding irritation of => functions' bodies being defined with -> syntax

What should main!'s type annotation/signature be? I remember in your talk you said => was designed to indicate effectfulness in anonymous contexts.

view this post on Zulip jan kili (Dec 18 2024 at 15:01):

Anthony Bullard said:

map : | List(a), (| a | b) | List(b)

:thinking:

As someone who's never used a Rust lambda, this is the only reference point I have for what : |args| might look like, and everyone said ew lol

view this post on Zulip jan kili (Dec 18 2024 at 15:03):

I'm enjoying the schemes cooking in this thread :blush:

view this post on Zulip Anthony Bullard (Dec 18 2024 at 15:04):

JanCVanB said:

Anthony Bullard said:

map : | List(a), (| a | b) | List(b)

:thinking:

As someone who's never used a Rust lambda, this is the only reference point I have for what : |args| might look like, and everyone said ew lol

That was meant to be an idea about how type annotations _could_ change. Don't want that at all. I personally would like Arg1, Arg2 (->/=>) RetType to remain for function type annotations

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 17:59):

Yeah, so it would be

map: List a, (a -> b) -> List b
map = |list, fn|
    ....

And called as

my_list.map(|a| a + 1)

view this post on Zulip Anthony Bullard (Dec 18 2024 at 22:40):

Would everyone agree that we are resolved to adopt |arg1, arg2| bodyExpr syntax for lambdas? Or are there any dissenters?

view this post on Zulip jan kili (Dec 18 2024 at 22:40):

Isn't it all function defs?

view this post on Zulip Anthony Bullard (Dec 18 2024 at 22:41):

Anthony Bullard said:

I leave this language the second <> are introduced

Yeah, a lambda == a function in Roc

view this post on Zulip Anthony Bullard (Dec 18 2024 at 22:41):

I'm working now on a fork of my AOC from this year rewritten in this new syntax

view this post on Zulip Anthony Bullard (Dec 18 2024 at 22:43):

Anthony Bullard said:

Anthony Bullard said:

I leave this language the second <> are introduced

Yeah, a lambda == a function in Roc

I mean a lambda is a ANONYMOUS function in Roc. I don't know if there is really a difference there since assigning it to a variable is the only way for a function to have a name in Roc.

view this post on Zulip jan kili (Dec 18 2024 at 22:43):

You can't annotate an anonymous function.

view this post on Zulip jan kili (Dec 18 2024 at 22:45):

I don't mean to be pedantic, though - the Python portion of my background hears "lambda" and points only at the anonymous inline ones.

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 22:50):

I would definitely say they are the same thing

You wouldn't call these different things:

x = 1
callThing(x)

callThing(1)

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 22:50):

The explicit 1 is not an anonymous number

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 22:50):

It just isn't saved in a variable

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 22:50):

In roc, we only have lambdas

view this post on Zulip Brendan Hansknecht (Dec 18 2024 at 22:50):

They may or may not get saved to a variable

view this post on Zulip Anthony Bullard (Dec 18 2024 at 22:50):

JanCVanB said:

You can't annotate an anonymous function.

This is a decent point, but I think in Roc it's safe to say all functions are lambdas

view this post on Zulip Anthony Bullard (Dec 18 2024 at 22:51):

Or what Brendan said :-)

view this post on Zulip jan kili (Dec 18 2024 at 23:21):

What if you prefix that code snippet with x : F32, then do they feel different?

view this post on Zulip jan kili (Dec 18 2024 at 23:23):

Nvm, in the map example above we have |a| a + 1 potentially specified by an outer function's annotation, so direct annotation isn't a differentiator. Roc functions are lambdas!

*"specified" here meaning (since I forget the official term for this) defined as something more specific than a -> b, such as Int -> Int

view this post on Zulip jan kili (Dec 18 2024 at 23:26):

It especially rings true if you can "always" multiline like

my_list.map(
    |a|
        a + 1
)

(whether or not the formatter one-lines it)

view this post on Zulip Sky Rose (Dec 19 2024 at 04:14):

Two things I really dislike about |a|:

view this post on Zulip Sam Mohr (Dec 19 2024 at 06:03):

view this post on Zulip Kilian Vounckx (Dec 19 2024 at 06:11):

Richard Feldman said:

I know it's unambiguous because it's only an infix operator when it's in the infix position, but still...

I'm shocked I never realized || is used for both 'or', and zero-arg lambda's in rust. They always felt like different things to me. Now I know, I have no idea if I will start to notice it more :big_smile:

view this post on Zulip jan kili (Dec 19 2024 at 07:11):

What if the args delimiter was already right in front of us this whole time? Can the pipe operator expand/overload to hold the args at definition time?

Feels like a cute compromise, but likely too spooky.

view this post on Zulip jan kili (Dec 19 2024 at 07:12):

Oof |arg1, arg2> looks so much worse than it did in my head.

view this post on Zulip jan kili (Dec 19 2024 at 07:14):

Maybe with whitespace | arg1, arg2 > isn't awful - nvm it's even worse lol

view this post on Zulip Sam Mohr (Dec 19 2024 at 07:20):

No no, let him cook

view this post on Zulip jan kili (Dec 19 2024 at 07:35):

|~ arg1 ~ arg2 ~>

view this post on Zulip jan kili (Dec 19 2024 at 07:36):

jk. I consider my above thought experiment dead on arrival, unless someone's seriously intrigued by the "||+-> compromise" idea.

view this post on Zulip Joshua Warner (Dec 26 2024 at 16:19):

One thing that we need to be careful with here, from a parsing perspective, is that (I think!) this idea of having closures delimited by || is fundamentally incompatible with space-separated function calls.

Otherwise, we're going to end up confusing foo |bar| baz (i.e. intended to be foo(|bar| baz)) with the disjunction of those three terms. And similar confusions are possible between the empty-args || form and the boolean or operator.

This means this will be hard to adopt incrementally / experiment with.

view this post on Zulip Joshua Warner (Dec 26 2024 at 16:21):

This has to come after space-separated calls are deprecated + removed.

view this post on Zulip Richard Feldman (Dec 26 2024 at 17:20):

I think that's fine because at first it'll need to be |{}| anyway

view this post on Zulip Richard Feldman (Dec 26 2024 at 17:21):

we don't currently have a concept of "zero-arg functions"

view this post on Zulip Richard Feldman (Dec 26 2024 at 17:22):

so one way to think of it is that introducing zero-arg functions with this syntax would only be possible if space calling has been removed from the parser

view this post on Zulip Joshua Warner (Dec 26 2024 at 17:28):

Not just that. This syntax doesn't work at all (zero arguments, one argument, more arguments) if we have space calling.

view this post on Zulip Joshua Warner (Dec 27 2024 at 01:09):

Actually, I take that back. It's not incompatible. They just can't be used together in the same call.


Last updated: Jun 16 2026 at 16:19 UTC