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,
}
the : <- looks a bit odd to me (so maybe there's a better way to convey "this is special")
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
Yeah, that clarifies my confusion when looking at the syntax.
Interesting. Did you have that reaction because it looks like backpassing?
I had the same reaction. It looked like backpassing. With the colons however it is more obvious it makes record fields
As an aside. I'm really looking forward to using this feature. Especially in combination with the Parser example
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)
}
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
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.
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.
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.
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.
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) }
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.
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
Can you give an example?
It's unclear what this should desugar to
succeed {
number: fn (<- parse number)
}
Why?
2 * (<- parse number) is really just Num.multiply 2 (<- parse number)
This syntax would just need to have priority to resolve before running the function.
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 <-.
Very true.
Not to mention it'd be a parsing nightmare
Anyway, I think this is a digression. Let's just assume that syntax isn't supported.
Yes
What if instead of <- we used something like :=?
That sounds fine, though maybe a bit less clear/more subtle (not sure if that is an issue or not).
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
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.
I think making it look like an assignment may specifically be the issue here.
I mean, we could set raw to the parsed number. Not that you'd ever want that.
So I think the space is good in this case.
:< lol
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.
I mean field assignment
Not a random def in the middle of a record
\* making it look too much like an assignment outside of a record, which in my mind are x = and x <-.
yeah :< or something a bit strange in general may work too.
Yeah, I think I would prefer : <-to that. Something about <- really says "effectful assignment" for me.
which since we're inside of a record, it can only be field assignment, so I personally don't think : is required
I can see how someone might confuse it with backpassing, but to use Record Builders you already have to learn a new syntax
If we block using the same name, i think it is a great syntax
I think it reads really confusingly of we don't
by that do you mean erroring on number <- ... or on raw : number? I guess the former
I'd be happy with that compromise
I like <- and I don't see myself writing code like the example in my PR
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,
}
Gotcha. Yeah, I think that's the best option. Basically reverting my PR.
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.
Something like this?
number = "42"
succeed {
number <- parse number,
raw: number,
# ^ ambiguous var error
}
Yeah
Oh, actually I really like that form of error message cause:
This could be a valid fix for example.
number = "42"
succeed {
raw: number,
number <- parse number,
}
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
honestly I think that makes it worth exploring in its own right, shadowing concerns aside :big_smile:
(I also don't love how number: <- looks though - I just think it's worth continuing to try out alternatives!)
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,
}
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
so it would be coupled to record syntax only; you couldn't use that special symbol just anywhere
only after the expression following the : in a record literal field label
that does kinda share the same concern about number: <- though, which is that it kinda suggests <- might be usable anywhere
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.
ah true
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,
}
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
does this make any sense? :thinking:
number = "42"
succeed {
raw: number,
number: -> parseNum number,
string: -> parseStr string,
}
so same as this, but flipped:
number = "42"
succeed {
raw: number,
number: <- parseNum number,
string: <- parseStr string,
}
I think it makes less sense than : <- but it does look more visually pleasing
hm, I think number: <- actually might be a parsing ambiguity :sweat_smile:
oh nm
it's fine because backpassing always requires a pattern to the left
number: <- parseNum number,
is kinda growing on me :big_smile:
Yeah, I can get used to it
I like that I can always tell these apart without further context:
number: <- parseNum number # always record builder
number <- parseNum number # always backpassing
I just feel like people are going to try (<- parseNum number, <- parseStr str) and be disappointed
it's possible, although that's one of those things we can always verify experimentally
if it turns out to be a significant problem in practice, we can revisit
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)
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:
Apparently, Idris has a !-notation which is like <- but you can put it anywhere
whaaaat
I need to overcome my shock at choosing to have !x mean something completely unrelated to negation and read how this works
oh I see
Actually, I guess that's for sequential stuff. For applicatives, there's Idiom brackets.
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
Yup
number = "42"
succeed {
raw: number,
number: [| parseNum number |],
string: [| parseStr string |],
}
Yeah, I don't like that :upside_down:
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
Yeah, let's do that
There can be 0 or more spaces between : and <-, right?
yeah
cool
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.
I think it's worth trying the other way to see if people get used to it
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:
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!
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.
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: ..."
Eating lunch by myself today, so I decided to wrap this up
delicious!
Weird idea? What about number <-: parse number?
Idk having a weird smiley face in the record seems a little odd hehe
Last updated: Jun 16 2026 at 16:19 UTC