Stream: ideas

Topic: lambda formatting


view this post on Zulip Anton (Nov 13 2023 at 13:35):

We regularly format code like this:

await Stdin.line \text ->
    Stdout.line "You just entered: \(text)"

But I think this is a lot better:

await
    Stdin.line
    \text -> Stdout.line "You just entered: \(text)"

It's now instantly clear that where the first argument ends and that we are working with two arguments.
I'd love to hear your thoughts :)

view this post on Zulip Isaac Van Doren (Nov 13 2023 at 14:08):

I prefer the first way because I’m used to it, it takes up two lines instead of three, and I think it emphasizes more the fact that the special lambda syntax can only be applied as the last argument.

In the second case because each parameter is on one line, it might seem like you could add another lambda below on a new line.

view this post on Zulip Anton (Nov 13 2023 at 14:27):

I prefer the first way because I’m used to it

Yeah, it mainly matters for beginners, but accommodating beginners is important

view this post on Zulip Anton (Nov 13 2023 at 14:32):

it takes up two lines instead of three

After spending time writing elm I've really come to prefer a "more lines but less dense" style.

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 14:37):

As a fool who never did web development or anything which asked for awaits, I have mixed feelings. The spread out version is certainly easier to parse in an abstract syntax tree way, but it also might be misleading for the beginners. It sorta looks like a keyword that marks control flow. Which, I guess, it almost is, except not at all because it's a function.

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 14:42):

However I do like how by separating the lambda in this context it helps make the structure of await more transparent, that you're passing two functions (or tasks I guess) which helps me parse what await is doing.

view this post on Zulip Anton (Nov 13 2023 at 14:42):

Highlighting may help in this case (I've edited the original snippets).
Another example:

List.map songs \song ->
      "Artist: \(song.artist)"

vs

List.map
    songs
    \song -> "Artist: \(song.artist)"

view this post on Zulip Anton (Nov 13 2023 at 14:43):

Note that with the songs/song names this does make our current style easy to understand, but the names won't always be this nice

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 14:44):

I feel that the first example works better for pipelining, whereas the later works better standalone?

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 14:47):

I mean you're definitely right that it can be very hard to tell where one thing ends and another begins with Roc's functional syntax. Very light and elegant and mathy, until you have to start building the syntax tree in your head to figure out anything. Reminds me too much of holding a bunch of index variables in my head doing low level array bullshit, too much mental juggling just to keep track of basic things.

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 14:50):

Also the structure generalises well for nesting. Not that we should be doing that too deep, but sometimes pulling that lambda out and naming it is more hassle and names and abstraction than is worth it, just another name you have to remember what it means and a function you need to find a place for.

view this post on Zulip Anton (Nov 13 2023 at 14:51):

Declan Joseph Maguire said:

I feel that the first example works better for pipelining, whereas the later works better standalone?

Do you mean when the List.map is the start of the pipeline or when it's inside one (= or at the end)?

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 14:56):

Tell me how you feel about this?

await
    Stdin.line \text ->
    Stdout.line "You just entered: \(text)"

Personally, I think it still reflects the structure while keeping the third line shorter, and conceptually I like letting a function body double for the function visually. Plus the obvious incompleteness of the initial \text carries the eye forwards to its completion

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 14:57):

Then again, looking at it now, it kinda makes the second argument look like a singular argument for Stdin.line. I can make my perception jump back and forwards like an optical illusion.

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 15:01):

Anton said:

Declan Joseph Maguire said:

I feel that the first example works better for pipelining, whereas the later works better standalone?

Do you mean when the List.map is the start of the pipeline or when it's inside one (= or at the end)?

I meant more generally, where the broken lambdas create a feeling of cascading down.

input
|> func1
|> func2 ChonkyTag |> \x ->
    x*x*x*+1
|> endFunc

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 15:03):

Not that that's super elegant, but that's sorta the point, an awkward example where that's the least-bad option imo

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 15:05):

Hey, following on from the leading example re: songs and artists - we should only use stupid standins when testing how formatting ought to go. While good variable names are crucial in real code and tutorials, formatting conventions should ideally handle those stupid cases where there isn't a good name for anything and it's a matter of minimising ugly, not maximising beauty.

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 15:10):

What if

wiggly
    bigly \x ->
    x*x*higgly

is the standard when wiggly is taking two arguments, bigly and the lambda, whereas

wiggly
    bigly \x ->
        x*x*higgly

is the case for the lambda as an argument of bigly. Function arguments, when line broken, are one indentation deeper than whatever they're fed to.

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 15:12):

Yes I know "pyramid of doom" and all that but if we go with tabs for indent then that can be mitigated per-developer, and also I think the risks are overstated and also we have backpassing and pipes so we have tools already that exist specifically to mitigate that.

view this post on Zulip Isaac Van Doren (Nov 13 2023 at 15:15):

One other benefit of the current approach is that it is reminiscent of the trailing closure syntax in groovy and swift (and probably others) so there are some familiarity points there

view this post on Zulip Anton (Nov 13 2023 at 15:16):

it kinda makes the second argument look like a singular argument for Stdin.line

Yeah, it seems like it would increase confusion about the boundaries.

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 15:17):

Isaac Van Doren said:

One other benefit of the current approach is that it is reminiscent of the trailing closure syntax in groovy and swift (and probably others) so there are some familiarity points there

Yes but as far as weirdness budgets go, this is paltry change, and I'd infintely prefer a few linebreaks for clarity than conventional confusion

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 15:18):

Anton said:

it kinda makes the second argument look like a singular argument for Stdin.line

Yeah, it seems like it would increase confusion about the boundaries.

I think my indent rule solves that, but I have no idea if it'd wreak chaos elsewhere

view this post on Zulip Richard Feldman (Nov 13 2023 at 15:24):

hm, I'm not convinced of the premise that the first way is harder for beginners :thinking:

that was how it was always formatted in CoffeeScript, which peaked at the 11th most popular language on GitHub. I remember people complaining about several things in CoffeeScript being confusing, but never that!

view this post on Zulip Richard Feldman (Nov 13 2023 at 15:25):

have we had any beginners express confusion about the status quo formatting?

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 15:27):

Well, me. At least with regards to more functional languages (I've done more functional style programming in nonfunctional languages) where you have trouble parsing nested function arguments, because it's all just a string of tokens separated by whitespace but you need to mentally track how many arguments everything is taking and where one function's arguments end and the next higher layer's begin.

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 15:30):

Certainly in the initial example, I find the second much clearer, the longer I look. Especially with stuff like await which has a high conceptual complexity already.

view this post on Zulip Anton (Nov 13 2023 at 15:31):

For my first example with await, I am pretty confident it will take a beginner significantly longer (with the current style) if you were to ask them to highlight the first argument in the await call.

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 15:32):

If anything, parsing the edges of nested function arguments is the single biggest difficulty I can see beginners having with Roc in specific.

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 15:34):

Also I still like my indenting convention because it recovers the old convention in simple cases while being extremely obvious in larger cases. Then again, I'm the least qualified person in this thread.

view this post on Zulip Richard Feldman (Nov 13 2023 at 15:43):

@Declan Joseph Maguire just to clarify, do you mean that the first time you saw a call like this in Roc, you were confused about what it was doing?

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 15:46):

No, I'm saying that I still get confused, even though I know intellectually how to parse it. Maybe I'm a tiny bit dyslexic, but needing to simulate a stack in my head is really hard, especially with something like await which I'm not very familiar with using. It's a lot of mental effort just to track syntax.

Not for the simplest examples, of course. It's only once you get nesting that it's a problem (usually).

view this post on Zulip Declan Joseph Maguire (Nov 13 2023 at 15:47):

Using indenting to communicate which random symbols are at the same level of the hierarchy makes it instantly a 100x easier to get. The longer I look, the more sense it makes.

view this post on Zulip Brendan Hansknecht (Nov 13 2023 at 15:53):

If you have something nested, won't there be parens (with the exception of a trailing lambda)?

view this post on Zulip Brendan Hansknecht (Nov 13 2023 at 15:54):

Also, I am not a fan of these changes because for larger lambdas they will add another level of indentation and I think too much indentation and lines starting to go off the edge of the screen is way worse for readability than anything mentioned here.

view this post on Zulip Brendan Hansknecht (Nov 13 2023 at 15:57):

On top of that, I think the await example should always be written with back passing and a pipeline operator.

text <- Stdin.line |> await
Stdout.line "You just entered: \(text)"

view this post on Zulip Brendan Hansknecht (Nov 13 2023 at 15:58):

If I could make roc formatter enforce that (maybe with an optin list of functions) I :100:% would.

view this post on Zulip Richard Feldman (Nov 13 2023 at 16:03):

Declan Joseph Maguire said:

Not for the simplest examples, of course. It's only once you get nesting that it's a problem (usually).

oh interesting - so, to clarify: it's not that you find List.map songs \song -> confusing, but rather that you find certain bigger examples confusing?

if so, do you happen to have an example of those bigger ones?

view this post on Zulip Anton (Nov 13 2023 at 16:14):

[...] for larger lambdas they will add another level of indentation and I think too much indentation and lines starting to go off the edge [...]

It's a possible pain point, it would be best to look at some real code to check. If your lambda is becoming large would it not be best to create a named function instead?

view this post on Zulip Anton (Nov 13 2023 at 16:17):

I think text <- Stdin.line |> await is awesome for experienced roc users but very difficult to understand for beginners. I would not be surprised if it took a complete beginner (but experienced imperative programmer) 10+ minutes to figure out the desugared version using google.

view this post on Zulip Richard Feldman (Nov 13 2023 at 17:49):

I totally agree that text <- Stdin.line |> await has significant learning curve for beginners, but at least in that case I think that's a case where the right solution is to focus on teaching rather than changing the formatting for everyone :big_smile:

view this post on Zulip Brendan Hansknecht (Nov 13 2023 at 19:13):

So named functions don't always work well in my experience. A lot of times lambdas are one offs that depend on local variables and really have no meaning elsewhere. Though you could make them a named function it feels quite contrived and hurts readability compared to directly going into the code. This is the worst when you have a linear flow with many dependencies. Prime example in roc is, of course, tasks. That said, lots of list transformations can be similar.

view this post on Zulip Isaac Van Doren (Nov 13 2023 at 20:03):

I am a big fan of being able to avoid naming things that don’t have a clear natural name

view this post on Zulip Declan Joseph Maguire (Nov 14 2023 at 08:06):

@Richard Feldman sorry for the slow reply, it was late here and I had work today. And yes, that's basically right. List.map songs \song -> is easy because the hierarchy of how things group is clear - a function, some data, and a lambda. I know how List.map works, and even if I didn't, it's clearly taking two arguments. In fact the way the lambda is at the end is really helpful, because the \song -> bit is clearly one of the arguments to List.map and there's nothing after the lambda, so I don't need to parse where the lambda's body ends and new arguments of List.map begin (were there any). Having the final argument be where a lambda would usually go is generally good for my ability to parse anyway.

However in the original example with await, Stdin.line from context feels like a function, so it feels like it should be consuming whatever is immediately to its right (in this case the lambda starting \text ->). The issue is that there's no way to know whether a fragment like func1 var9 is a function acting on a variable, or two arguments to a prior function without fully understanding the context. Unless you have a very strong understanding of what's a function, how many arguments they're taking, and are accurately keeping track of this growing structure as you scan through, it becomes very easy to lose track of what starts where.

To make things worse, the syntax highlighting didn't highlight the text def hidden in the blob of string, so it looks like Stdout.line is accepting a literal whose output would then be the output of the lambda (at which point you realise that's stupid and go back and figure out where you messed up). This syntax highlighting thing is obviously a totally separate factor, but even then it could have been some other thing that added ambiguity.

The whole thing imposes a lot of cognitive load for me, disproportionate to the actual complexity of the underlying ideas. That's why I like the idea of multiline expressions having arguments indented relative to their functions - I don't need to spend any time figuring out what the code says, and I can just read what it means. In fact the initial post the second example was a lightbulb moment for me to finally get how await works in a loose sense, such is its clarity.

I don't think this breaking and indenting needs to be done for every function call, obviously. Usually it's fine when its a single-liner. But I start getting confused when they get larger, which is about the point people start breaking their lines up, in ways that visually imply false interpretations.

At its worst, it becomes the code equivalent of a garden-path sentence (google them if you're unfamiliar).

view this post on Zulip Declan Joseph Maguire (Nov 14 2023 at 08:17):

"Garden-path functions", if you will.

view this post on Zulip Anton (Nov 14 2023 at 09:54):

It's worth checking out the examples of garden-path sentences, that really brought it home for me.

So, to rephrase the issue; with the formatting like below, you are most likely going to fail parsing (mentally) this correctly without prior roc experience.

await Stdin.line \text ->
    Stdout.line "You just entered: \(text)"

It requires you to shift into slow thinking mode.

view this post on Zulip Declan Joseph Maguire (Nov 14 2023 at 10:03):

Yes, or it would if I hadn't spent enough time staring at it to start internalising it. However, I am fully confident this would repeat if I spent time away and was given a different, but equivalent example. And yes, I 100% urge anyone who doesn't understand why I'm whining to start looking at the garden path sentences Anton linked, or others.

Also, I thought it was obvious, but when I'm talking about parsing here I mean mentally. The struggle I have going into slow thinking mode for complex function calls in Roc is similar to more C style languages when you get chunks of brackets like )))( whose owners are hard to figure out and which are even hard to count. It's the same mental strain.

view this post on Zulip Declan Joseph Maguire (Nov 14 2023 at 10:06):

If await's first argument weren't a function (task), it would be easier and that example would probably be fine, like with the list.map songs \song ->... example. However for consistency's sake the more general standard is better than one that's pretty for cherrypicked cases.

view this post on Zulip Hannes (Nov 14 2023 at 12:45):

Just to throw my opinion into this pile, I put parentheses around every lambda to make it clearer what I mean, otherwise I waste time trying to parse the syntax in my head and figure out where the lambda ends. I don't think this affects this decision particularly though, so feel free to ignore me :sweat_smile:

view this post on Zulip Hannes (Nov 14 2023 at 12:48):

Actually having said that, I think I agree with @Anton's original message, having the lambda on its own line might make the syntax obvious enough to me that I wouldn't need the parens

view this post on Zulip Hannes (Nov 14 2023 at 12:50):

I think the reason i started putting parens around lambdas is exactly because I found this kind of formatting hard to parse, but I love autoformatters so much that I'm used to just adding parens as "formatting hints" in other languages

view this post on Zulip Declan Joseph Maguire (Nov 14 2023 at 12:50):

I feel like some more advanced syntax highlighting could help, something that is able to subtly prime the eye to see the right structure, but I'm not sure the best way to do that. And in any case, I think you'd want to address deeper issues of formatting syntax rather than trying to paper over with highlighting, so that's really a separate discussion.

view this post on Zulip Richard Feldman (Nov 14 2023 at 13:54):

hm, do we have an example of this type of pattern from some actual code?

view this post on Zulip Richard Feldman (Nov 14 2023 at 13:54):

(with await in particular it's always done with backpassing anyway)

view this post on Zulip Declan Joseph Maguire (Nov 14 2023 at 14:06):

For me it's not a problem of any particular pattern. I was just drilling into the given examples because they were in front of me and they perfectly exemplify the the type of confusion that can happen. In spite of the name of this thread, the issues I have with garden-path function calls are only sometimes related to lambdas, although they exacerbate the problem with their visual complexity and low visual cohesion. It crops up whenever multiple bracketings become visually plausible (even though only one is logically possible upon a more effortful inspection).

view this post on Zulip Richard Feldman (Nov 14 2023 at 14:06):

I ask in part because it sounds like the source of the confusion is specific to that example and not something general:

Declan Joseph Maguire said:

List.map songs \song -> is easy because the hierarchy of how things group is clear - a function, some data, and a lambda. I know how List.map works, and even if I didn't, it's clearly taking two arguments. In fact the way the lambda is at the end is really helpful, because the \song -> bit is clearly one of the arguments to List.map and there's nothing after the lambda, so I don't need to parse where the lambda's body ends and new arguments of List.map begin (were there any). Having the final argument be where a lambda would usually go is generally good for my ability to parse anyway.

However in the original example with await, Stdin.line from context feels like a function, so it feels like it should be consuming whatever is immediately to its right (in this case the lambda starting \text ->). The issue is that there's no way to know whether a fragment like func1 var9 is a function acting on a variable, or two arguments to a prior function without fully understanding the context. Unless you have a very strong understanding of what's a function, how many arguments they're taking, and are accurately keeping track of this growing structure as you scan through, it becomes very easy to lose track of what starts where.

I think the names in these examples have a lot to do with the difference in terms of "what looks like a function."

Like I don't think many people are looking at songs and assuming it's a function, whereas I wouldn't say the same of Stdin.line

view this post on Zulip Declan Joseph Maguire (Nov 14 2023 at 14:08):

That's very true, but I think that says more to the intentionally designed simplicity of the songs example rather than the await example representing an abnormal edge case.

view this post on Zulip Declan Joseph Maguire (Nov 14 2023 at 14:10):

Like, to reference the actual variable names, songs is obviously a noun, which contextually would not refer to some process and thus not be a function. Not all variable names get to be that clear, despite our best efforts. Naming is the second hardest problem in computer science as we know.

view this post on Zulip Declan Joseph Maguire (Nov 14 2023 at 14:13):

So obviously the clarity of syntactic parsing (mentally) is not independent from the semantic content of the name, but that semantic content is often highly unreliable as a syntactic guide. Especially when reading someone else's code - either due to their incompetence, or just different ways of thinking that are unintuitive to you.

view this post on Zulip Declan Joseph Maguire (Nov 14 2023 at 14:14):

Relying on good variable names to disambiguate syntax is like not wearing a seatbelt because you're an excellent driver, so why worry about a crash?

view this post on Zulip Richard Feldman (Nov 14 2023 at 14:17):

well but of the two examples, the songs one comes up all the time, whereas the main reason backpassing was introduced to the language was that nobody wanted to write await like that :big_smile:

view this post on Zulip Richard Feldman (Nov 14 2023 at 14:18):

so if the common example is clear but the one that never comes up is confusing, I think it's important that we don't overgeneralize, and instead find a specific example that's both confusing to read and also something that actually comes up!

view this post on Zulip Declan Joseph Maguire (Nov 14 2023 at 14:19):

I'll do a little hunting to find a good example, or construct a highly plausible one. Brb

view this post on Zulip Declan Joseph Maguire (Nov 14 2023 at 14:20):

Though warning I might have to bail again because timezones and 9-5 working humans need sleep

view this post on Zulip Richard Feldman (Nov 14 2023 at 14:20):

haha no worries!

view this post on Zulip Declan Joseph Maguire (Nov 14 2023 at 14:24):

I've been way busier than I though I'd be when I came here, so most of the Roc I've been writing has been simple proof of concepts (also windows has been giving conniptions regarding linux subsystem, unrelated to Roc). What would you say is the best representative repo of idiomatic Roc code we have? Can you point me to it? I'm having one of those "I SWEAR I saw it somewhere" moments

view this post on Zulip Declan Joseph Maguire (Nov 14 2023 at 14:31):

Wait I can just find examples listed in these threads. Probably bedtime for me, I'll think more clearly in the morning.

view this post on Zulip Anton (Nov 14 2023 at 14:33):

Good night :)

view this post on Zulip Anton (Nov 14 2023 at 14:34):

The examples repo may also be useful

view this post on Zulip Jacob (Nov 14 2023 at 14:36):

I think the 2nd example is clearer also, but I'd rather use backpassing syntax than either of them. Or if it really bugged me, just put parentheses around both arguments.

view this post on Zulip Eli Dowling (Nov 14 2023 at 20:48):

For me coming from other fp languages I much prefer the first example
It's very visually clear because any line that ends in -> the next indented line is clearly a lambda body.
The second example it's less immediately obvious at a glance where the lambda body is
I think the first has this nice clean separation, i see the -> and an indent and i know I'm now in a new context
context 1 ->
context 2

but lastly back-passing is obviously better for async await, so i think is is more relevant to things like List.map

view this post on Zulip Brendan Hansknecht (Nov 14 2023 at 23:33):

However in the original example with await, Stdin.line from context feels like a function, so it feels like it should be consuming whatever is immediately to its right

So when it comes to parsing something like:

await Stdin.line a b c d testing123 \text -> ...

it doesn't matter how many random things I put between await and the closure. Without parens, every single value in that entire line is passed to await. Even if Stdin.line was a function, it would be a function that is being passed to await, not being called.

So I am pretty sure that this syntax is completely clear. It should only take a simple explanation to show that it is impossible for Stdin.line to be taking args in the above printout.

If Stdin.line was being called and took args, it would look like:

await (Stdin.line a b c d) testing123 \text -> ...

The parens directly show the nested function call.


Last updated: Jun 16 2026 at 16:19 UTC