Stream: ideas

Topic: label syntax for record builders


view this post on Zulip Richard Feldman (May 16 2023 at 19:49):

in a comment on https://github.com/roc-lang/roc/pull/5415 @Brendan Hansknecht wrote:

This feels quite strange to enable given we don't allow shadowing in general.

I had the same reaction. I wonder if we should consider tweaking the syntax to make it clearer that it's a field name? For example, maybe:

number = "42"

succeed {
    number: <- parse number,
    raw: number,
}

view this post on Zulip Richard Feldman (May 16 2023 at 19:49):

the : <- looks a bit odd to me (so maybe there's a better way to convey "this is special")

view this post on Zulip Richard Feldman (May 16 2023 at 19:50):

but as soon as I see number: there, I immediately no longer think "wait, that's shadowing" - and, arguably more importantly, I no longer might think that raw: number might actually refer to the number: in the record - which, in the absence of the : after number: would be a potentially easy mistake to make

view this post on Zulip Brendan Hansknecht (May 16 2023 at 20:02):

Yeah, that clarifies my confusion when looking at the syntax.

view this post on Zulip Agus Zubiaga (May 16 2023 at 21:34):

Interesting. Did you have that reaction because it looks like backpassing?

view this post on Zulip Kilian Vounckx (May 16 2023 at 21:36):

I had the same reaction. It looked like backpassing. With the colons however it is more obvious it makes record fields

view this post on Zulip Kilian Vounckx (May 16 2023 at 21:37):

As an aside. I'm really looking forward to using this feature. Especially in combination with the Parser example

view this post on Zulip Agus Zubiaga (May 16 2023 at 21:40):

I don't love the : <- syntax because it makes the <- look like it's part of the expression, and someone might think you can do this:

succeed {
    number: 2 * (<- parse number)
}

view this post on Zulip Kilian Vounckx (May 16 2023 at 21:44):

Not sure on the syntax either, however with only the arrow, I didn't get what it meant. With the colon I got it immediately. I think that by using it I would understand it eventually as well, but with the colon I understood just by looking

view this post on Zulip Brendan Hansknecht (May 16 2023 at 21:58):

How I see it is that backpassing reads as a weird way to write =.

these two feel equivalent

x = someFunc 3
x <- someAwaitedFunc 3

So when I read the current syntax it goes from:

succeed {
    number <- parse number,
    raw: number,
}

To essentially:

succeed {
    number = parse number,
    raw: number,
}

So it really feels like raw is getting set to the parsed number, not the original number.
Of course that is the wrong reading of the syntax, but it matches the rough rule of thumb that is gained from working with backpassing.

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:00):

Adding the :, though ugly, makes it immediately clear that we are setting a record field. The only place you would see number: someExpression is when setting a record field.

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:01):

I don't know the best syntax, cause if you proced to apply my rough rule of thumb to the proposed syntax, it goes from:

succeed {
    number: <- parse number,
    raw: number,
}

to:

succeed {
    number: = parse number,
    raw: number,
}

Which is fundamentally just a broken syntax.

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:02):

I see your point. That said, I think people are unlikely to write code like this. I just came up with that example to test the canonicalizer.

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:04):

What do you think about this point @Brendan Hansknecht ?
Agus Zubiaga said:

I don't love the : <- syntax because it makes the <- look like it's part of the expression, and someone might think you can do this:

succeed {
    number: 2 * (<- parse number)
}

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:05):

Aside, this code:

succeed {
    number: 2 * (<- parse number)
}

actually makes total sense to me even if we don't want to support it.
It would be saying, applicatively run parse number multiply it by 2 and then store it in the number record field.
It feels like it would be a syntax for anonymous backpassing.

x <- Stdin.getInt |> Task.await
y = x * 2

# could be with anonymous backpassing

y = 2 * (<- Stdin.getInt |> Task.await)

Not saying we should at all try to support it, just saying that it is immediately what I think when reading it.

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:06):

I think we just can't support that syntax. Sure it works in that example, but as soon as you wrap (<- ...) in a function we would desugar to the wrong thing

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:07):

Can you give an example?

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:09):

It's unclear what this should desugar to

succeed {
    number: fn (<- parse number)
}

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:10):

Why?
2 * (<- parse number) is really just Num.multiply 2 (<- parse number)

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:11):

This syntax would just need to have priority to resolve before running the function.

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:12):

True. I guess it also applies there. I just think in a world where you can do (<- ...), it doesn't make sense to limit it to records. But if you don't, the compiler wouldn't know where to apply the function on the RHS of <-.

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:12):

Very true.

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:12):

Not to mention it'd be a parsing nightmare

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:12):

Anyway, I think this is a digression. Let's just assume that syntax isn't supported.

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:13):

Yes

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:13):

What if instead of <- we used something like :=?

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:14):

That sounds fine, though maybe a bit less clear/more subtle (not sure if that is an issue or not).

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:14):

I'd like to use a symbol without a space in the middle, to make it look like it's a special kind of assignment, not that it's part of the expression

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:15):

So we get:

number = "42"

succeed {
    number := parse number,
    raw: number,
}

hmm....maybe that doesn't work. := feels too much like an assignment and again like raw would be getting set to the parsed number.

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:16):

I think making it look like an assignment may specifically be the issue here.

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:16):

I mean, we could set raw to the parsed number. Not that you'd ever want that.

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:16):

So I think the space is good in this case.

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:18):

:< lol

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:18):

Brendan Hansknecht said:

I think making it look like an assignment may specifically be the issue here.

I do think about it as a kind of assignment though.

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:19):

I mean field assignment

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:20):

Not a random def in the middle of a record

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:20):

\* making it look too much like an assignment outside of a record, which in my mind are x = and x <-.

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:22):

yeah :< or something a bit strange in general may work too.

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:43):

Yeah, I think I would prefer : <-to that. Something about <- really says "effectful assignment" for me.

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:48):

which since we're inside of a record, it can only be field assignment, so I personally don't think : is required

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:50):

I can see how someone might confuse it with backpassing, but to use Record Builders you already have to learn a new syntax

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:52):

If we block using the same name, i think it is a great syntax

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:53):

I think it reads really confusingly of we don't

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:55):

by that do you mean erroring on number <- ... or on raw : number? I guess the former

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:58):

I'd be happy with that compromise

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:59):

I like <- and I don't see myself writing code like the example in my PR

view this post on Zulip Brendan Hansknecht (May 16 2023 at 22:59):

Erroring on a variable named number existing in the same scope as a recorder builder { number <- ... }

So would need to be:

numberStr = "42"

succeed {
    number <- parse numberStr,
    Raw: numberStr,
}

view this post on Zulip Agus Zubiaga (May 16 2023 at 22:59):

Gotcha. Yeah, I think that's the best option. Basically reverting my PR.

view this post on Zulip Brendan Hansknecht (May 16 2023 at 23:00):

Though also probably we be fine to block it from other record fields or some other solution that avoid the duplicate name and potential confusion on variable vs field.

view this post on Zulip Agus Zubiaga (May 16 2023 at 23:02):

Something like this?

number = "42"

succeed {
    number <- parse number,
    raw: number,
    #     ^ ambiguous var error
}

view this post on Zulip Brendan Hansknecht (May 16 2023 at 23:03):

Yeah

view this post on Zulip Brendan Hansknecht (May 16 2023 at 23:29):

Oh, actually I really like that form of error message cause:

  1. I think it will very very rarely come up in practice
  2. In many cases, it should be fixable by reordering fields
  3. When it isn't you just need to rename one value

This could be a valid fix for example.

number = "42"

succeed {
    raw: number,
    number <- parse number,
}

view this post on Zulip Richard Feldman (May 16 2023 at 23:31):

even setting aside the shadowing concern, the thing I like about number: is that it's more obvious that it's making record fields. For example:

Kilian Vounckx said:

I had the same reaction. It looked like backpassing. With the colons however it is more obvious it makes record fields

view this post on Zulip Richard Feldman (May 16 2023 at 23:31):

honestly I think that makes it worth exploring in its own right, shadowing concerns aside :big_smile:

view this post on Zulip Richard Feldman (May 16 2023 at 23:32):

(I also don't love how number: <- looks though - I just think it's worth continuing to try out alternatives!)

view this post on Zulip Richard Feldman (May 16 2023 at 23:43):

a possible direction: since each of these need a Task.batch, we could put a special operator after that

number = "42"

succeed {
    raw: number,
    number: Task.batch | parseNum number,
    string: Task.batch | parseStr string,
}

view this post on Zulip Richard Feldman (May 16 2023 at 23:44):

not saying | specifically is the best one, but the general idea is that [field]: [expr] [symbol] [expr] becomes the syntax that lets you know it's a record builder

view this post on Zulip Richard Feldman (May 16 2023 at 23:44):

so it would be coupled to record syntax only; you couldn't use that special symbol just anywhere

view this post on Zulip Richard Feldman (May 16 2023 at 23:45):

only after the expression following the : in a record literal field label

view this post on Zulip Richard Feldman (May 16 2023 at 23:45):

that does kinda share the same concern about number: <- though, which is that it kinda suggests <- might be usable anywhere

view this post on Zulip Agus Zubiaga (May 16 2023 at 23:47):

You don't necessarily need a Task.batch though. In my example, parse does both apply and construct the parser. Some applicative libs such as NoRedInk/elm-json-decode-pipeline andelm/parser use this pattern.

view this post on Zulip Richard Feldman (May 16 2023 at 23:48):

ah true

view this post on Zulip Richard Feldman (May 16 2023 at 23:59):

here's an idea I don't like, but which I'll put out there anyway just to get it out of my head:

number = "42"

succeed {
    raw: number,
    number:: parseNum number,
    string:: parseStr string,
}

view this post on Zulip Agus Zubiaga (May 17 2023 at 00:02):

Here's an idea I don't know if I like:

succeed {
    raw: number,
    number: apply parseNum number,
    string: apply parseStr string,
}

apply being a reserved keyword

view this post on Zulip Richard Feldman (May 17 2023 at 00:04):

does this make any sense? :thinking:

number = "42"

succeed {
    raw: number,
    number: -> parseNum number,
    string: -> parseStr string,
}

view this post on Zulip Richard Feldman (May 17 2023 at 00:06):

so same as this, but flipped:

number = "42"

succeed {
    raw: number,
    number: <- parseNum number,
    string: <- parseStr string,
}

view this post on Zulip Agus Zubiaga (May 17 2023 at 00:06):

I think it makes less sense than : <- but it does look more visually pleasing

view this post on Zulip Richard Feldman (May 17 2023 at 00:08):

hm, I think number: <- actually might be a parsing ambiguity :sweat_smile:

view this post on Zulip Richard Feldman (May 17 2023 at 00:08):

oh nm

view this post on Zulip Richard Feldman (May 17 2023 at 00:08):

it's fine because backpassing always requires a pattern to the left

view this post on Zulip Richard Feldman (May 17 2023 at 00:09):

number: <- parseNum number,

is kinda growing on me :big_smile:

view this post on Zulip Agus Zubiaga (May 17 2023 at 00:10):

Yeah, I can get used to it

view this post on Zulip Richard Feldman (May 17 2023 at 00:10):

I like that I can always tell these apart without further context:

number: <- parseNum number # always record builder
number <- parseNum number # always backpassing

view this post on Zulip Agus Zubiaga (May 17 2023 at 00:11):

I just feel like people are going to try (<- parseNum number, <- parseStr str) and be disappointed

view this post on Zulip Richard Feldman (May 17 2023 at 00:11):

it's possible, although that's one of those things we can always verify experimentally

view this post on Zulip Richard Feldman (May 17 2023 at 00:11):

if it turns out to be a significant problem in practice, we can revisit

view this post on Zulip Richard Feldman (May 17 2023 at 00:12):

which sounds reasonable to me, considering number: <- does solve a couple of existing known pain points mentioned in this thread (shadowing concern, not realizing that number <- was going into a record field)

view this post on Zulip Richard Feldman (May 17 2023 at 00:13):

and it's uncharted syntax territory anyway, so it's not like we can just fall back on a syntax another language uses for this :sweat_smile:

view this post on Zulip Agus Zubiaga (May 17 2023 at 00:13):

Apparently, Idris has a !-notation which is like <- but you can put it anywhere

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

whaaaat

view this post on Zulip Richard Feldman (May 17 2023 at 00:15):

I need to overcome my shock at choosing to have !x mean something completely unrelated to negation and read how this works

view this post on Zulip Richard Feldman (May 17 2023 at 00:18):

oh I see

view this post on Zulip Agus Zubiaga (May 17 2023 at 00:19):

Actually, I guess that's for sequential stuff. For applicatives, there's Idiom brackets.

view this post on Zulip Richard Feldman (May 17 2023 at 00:19):

yeah so we couldn't do this because it relies on higher-kinded types to know what bind (or apply) and pure (or suceced) are

view this post on Zulip Agus Zubiaga (May 17 2023 at 00:19):

Yup

view this post on Zulip Agus Zubiaga (May 17 2023 at 00:21):

number = "42"

succeed {
    raw: number,
    number: [| parseNum number |],
    string: [| parseStr string |],
}

Yeah, I don't like that :upside_down:

view this post on Zulip Richard Feldman (May 17 2023 at 00:22):

how do you feel about trying number: <- and seeing how it feels in practice? if it was a standalone PR, we could revert back to the current syntax if it ends up seeming worse in practice

view this post on Zulip Agus Zubiaga (May 17 2023 at 00:22):

Yeah, let's do that

view this post on Zulip Agus Zubiaga (May 17 2023 at 00:23):

There can be 0 or more spaces between : and <-, right?

view this post on Zulip Richard Feldman (May 17 2023 at 00:23):

yeah

view this post on Zulip Agus Zubiaga (May 17 2023 at 00:23):

cool

view this post on Zulip Sky Rose (May 17 2023 at 00:24):

I don't mind number <-.

x <- Stdin.getInt |> Task.await looks like variable assignment because it shows up in contexts that look like you should be doing variable assignment.
number <- parseNumber numberStr looks like field assignment because you do it inside {}, where you assign record fields.

It's as much an issue with shadowing as this case:

number = 42
r = {
  number: 41,
  biggerNumber: number,
}

There are already rules about how field <- isn't assigning a value you can use in future calculations.

view this post on Zulip Richard Feldman (May 17 2023 at 00:25):

I think it's worth trying the other way to see if people get used to it

view this post on Zulip Richard Feldman (May 17 2023 at 00:26):

I think it's plausible that either could work, but I definitely think number: is clearer that it's a field label - you don't even need any context to infer it :big_smile:

view this post on Zulip Agus Zubiaga (May 17 2023 at 02:31):

I updated the parser to the new syntax and it seems to be working fine. I need to leave now but I will work on the formatter tomorrow and make a PR!

view this post on Zulip Georges Boris (May 17 2023 at 08:37):

fwiw I had the same confusion when seeing the previous syntax, I immediately thought that ordering would be a problem since we could in theory create dependencies between parallel requests for instance (bc I also thought of ordered assignments)

the new syntax with : <- seems to solve it imo, we might experiment stuff but I think the less syntax we add the to language the better and this one adds no new special operators.

view this post on Zulip Georges Boris (May 17 2023 at 08:40):

it should be easy to throw meaningful error messages if the user tries to (<- fn x), right? "it looks like you're trying to use backpassing in an unsupported place. these are the only two supported ways of using backpassing in Roc: ..."

view this post on Zulip Agus Zubiaga (May 17 2023 at 17:35):

Eating lunch by myself today, so I decided to wrap this up

view this post on Zulip Richard Feldman (May 17 2023 at 18:36):

delicious!

view this post on Zulip Bryce Miller (May 18 2023 at 12:45):

Weird idea? What about number <-: parse number?

view this post on Zulip Bryce Miller (May 18 2023 at 12:45):

Idk having a weird smiley face in the record seems a little odd hehe


Last updated: Jun 16 2026 at 16:19 UTC