so a common thing to want to do in record updates is:
{ record & foo: record.foo + 1 }
I've been working on some examples of translating for loops in other languages into Roc (with a goal of having a "recipe book" page of like "here's how to go from the thing you're familiar with to the Roc equivalent, and for loops are a super common thing in other languages)
and inside a walk, doing stuff like { state & count: state.count + 1 } (and similar) is common
here's an idea for some syntax to make that more concise:
Modify variables in nested loops.
nonempty = 0
elems = []
for list in lists:
if len(list) > 0:
nonempty += 1
for elem in list:
elems.push(elem)
{ nonempty, elems } =
List.walk lists { nonempty: 0, elems: [] } \state, list ->
{ state &
nonempty: + if List.isEmpty list then 0 else 1,
elems: |> List.concat list
}
hopefully the syntax idea is so straightforward that it doesn't need explaining :big_smile:
any thoughts welcome!
This seems pretty straightforward and nice
Could be even better if we made the formatter outdent { state & … } in a lambda:
{ nonempty, elems } =
List.walk lists { nonempty: 0, elems: [] } \state, list -> { state &
nonempty: + if List.isEmpty list then 0 else 1,
elems: |> List.concat list
}
I don’t remember if it was on Zulip or in person, but I think we discussed allowing if/when at the same level of fields, so you didn’t have to “centralize” all the field updates in one branch
yeah I like those ideas! I think there were some parsing challenges with it, but maybe @Joshua Warner's blocks PR might have changed that?
Yeah, I think the issue was that using : would conflict with a def annotation, so we would need a different operator
a related idea I had was:
{ state &
nonempty: if !List.isEmpty list then
state.nonempty + 1
}
so "if without else" and then the implied else branch is "whatever the field currently is"
I get it but the else-less if being on the RHS of : is a little weird
yeah I'm not sure that one is a good idea :laughing:
a downside of the original idea is that although this seems nice and straightforward:
{ state &
nonempty: + if List.isEmpty list then 0 else 1,
elems: |> List.concat list
}
...it's not so nice with an alternative record update syntax we've discussed elsewhere:
state & {
nonempty: + if List.isEmpty list then 0 else 1,
elems: |> List.concat list
}
that second one requires type system changes, and it's not clear it's the right design, but I wanted to note that this idea doesn't work as nicely with it because now it looks like there's a standalone record with strange fields
I've definitely felt this one being awkward before, so I like the idea here! A few thoughts:
&foo -> \record, foo -> { record & foo }? else-less if is weird, and inconsistent with other Roc code.state & { ... } change is unnecessary IMO, between record builders and record updates we now have a consistency of prefixes within record literals that makes sense.On 2., I think that there's probably not a way to do it without a new operator (I think), at which point it becomes not worth adding, considering how niche the usage would be.
I think this could be confusing with operators like + and - because it could be confused with unary operators. Consider a statement like
remaining -= 1
If I understand the suggested syntax correctly, this would translate to
{ state & remaining: -1 }
How can this be distinguished from just setting remaining to the constant value -1?
Maybe it would make more sense to have a shorthand for the current value. Let's say .:
{ state & remaining: . - 1 }
In effect, . would just be a locally bound variable, a shorthand for state.remaining. Maybe a different character would be better suited, I'm not sure.
You're suggesting something like it from Kotlin? I can see the value
I think it's not necessary. There are only a few situations where this would be a little confusing, and you can always use the original field a la { state & remaining: state.remaining - 1 }
Also, we parse f -1 as a function call with an arg of -1:
» f = 5
5 : Num *
» f -1
── TOO MANY ARGS ───────────────────────────────────────────────────────────────
The f value is not a function, but it was given 1 argument:
8│ f -1
I wasn't necessarily only thinking about constants like -1 itself, but about the fact that even in the original proposal, the + looks like a unary operator, and I think this would be too subtle a change. IIRC, very early versions of C actually used =+ instead of +=but changed the operator for the same reason.
A subtle difference like f -1 being parsed differently from f - 1 is fine when the compiler can catch it. But { state & remaining: -1 } and { state & remaining: - 1} would both compile and run, they'd just behave differently.
Maybe it would make more sense to have a shorthand for the current value. Let's say
.:
I'm also in favor of something like this, I think it's easier to grasp.
Scala also does something similar with _:
val numbers = List(1, 2, 3, 4, 5)
val doubled = numbers.map(_ * 2)
Full example:
{ state &
nonempty: _ + if List.isEmpty list then 0 else 1,
elems: List.concat _ list
}
Hm, if that works for record fields, I think it wouldn't work for arbitrary lambdas.
How could we tell if you mean...
{ state &
nonempty: _ + if List.isEmpty list then 0 else 1,
elems: List.concat state.elems list
}
or
{ state &
nonempty: _ + if List.isEmpty list then 0 else 1,
elems: \x -> List.concat x list
}
?
Hm, if that works for record fields, I think it wouldn't work for arbitrary lambdas.
Yeah, I would only use it for record fields
I also vote for _ (if we do this), that's what Gleam did as well.
@Agus Zubiaga if we only allow parsing of _ <binary operator> <expr> then we don't have to worry about confusion there IMO
Anton said:
I'm also in favor of something like this, I think it's easier to grasp.
Scala also does something similar with_:val numbers = List(1, 2, 3, 4, 5) val doubled = numbers.map(_ * 2)
Something like this wouldn't work, though
Gleam's function capture where you replace an argument with _ feels like a totally separate thing, though
Right, my point is that if we have that, we can't use _ to mean "the current record field"
because they both desugar to different things
Yep
And if we have these fields mean different things, that seems really confusing.
{ recordToUpdate &
foo: _ |> List.concat 123,
bar: List.concat _ 123,
}
I think the function capture approach works in Gleam because they have parentheses around their function calls. I don't know how we could add it to Roc without it being tricky for newcomers. We already dropped curried functions so we could dissuade users from writing confusingly elided code
So anyway, I think if we dedicate _ for this record update elision behavior, then I'm okay with not having function capture.
I'm not a fan of this. While it wouldn't be hard to learn. I think it adds a very strange looking syntax that feels like we are just chasing minimum character count.
That's to the root idea, not the discussion afterwards.
Imo this kind of syntax makes refactoring less convenient by introducing inconsistency between record-update functions and general functions.
I think it corresponds to the first point in the @Sam Mohr’s list
Could we somehow make the record field extraction syntax work here. Something based off of:
{record & foo: .foo + 23}
Cause most of the noise is repeating the record name.
Isn’t .foo already a getter?
Yep., but I think using that syntax in two different ways imor something similar to it is better than fully dropping the record and field name
So just throwing out the rough idea
Richard Feldman schrieb:
a related idea I had was:
{ state & nonempty: if !List.isEmpty list then state.nonempty + 1 }so "if without else" and then the implied else branch is "whatever the field currently is"
Using the _ syntax would also make this relatively straightforward:
{ state &
nonempty: if !List.isEmpty then _ + 1 else _
}
This is the syntax I referenced earlier btw:
{ nonempty, elems } =
List.walk lists { nonempty: 0, elems: [] } \state, list -> { state &
elems: List.concat state.elems list
if !List.isEmpty list then
nonempty: state.nonempty + 1
}
What's tricky with this one is that : conflicts with a def annotation
I think it's really cool that you can update specific fields under different branches
I previously thought: what if to replace : with = in records and in general make the record declaration work just like a function namespace. So any declarations inside this syntax are then become fields of a record object.
record = {
counter = 0
}
record.counter == 0
With this “records as namespaces” concept, it’s possible to do smth like this (assuming shadowing is allowed)
record = {
counter = 0
}
newRecord = record & {
counter = counter + 1
}
newRecord.counter == 1
But then I decided that it would complicate a lot of other places. Anyway, wanted to share, maybe it will inspire a better idea
A lot of the examples above involve implicitly or explicitly re-defining a record, e.g. where shadowing would be useful anyway.
To that end, what if we made "field update syntax" (from procedural languages) do what you'd expect in many cases?
Something like:
state.nonempty = state.nonempty + if List.isEmpty list then 0 else 1
state.elems = List.concat state.elems list
state
Or even:
state.nonempty += if List.isEmpty list then 0 else 1
state.elems |>= List.concat list
state
The semantics would be:
state is required to be a variable bound in the current "defs" scope (i.e. no redefining things in outer scopes, no mutating parts of the heap, etc)stateDesugared version:
state = { state & nonempty: state.nonempty + if List.isEmpty list then 0 else 1}
state = { state & elems: List.concat state.elems list }
state
+= feels wrong to me. But this makes. A lot of sense.
I think it would definitely require general += support and such
x += y
That technically would be valid with shadowing.
I like that a lot!
I do think one gotcha we are introducing with syntax like this is that shadowing does not escape scope. So it can look like you are mutating something, but the value isn't actually replaced at the outer scope.
x = 3
y =
x += 1
x
# this prints 3
dbg x
This affects both the record and raw value syntax. It simply looks less correct and is an easy bug to hit.
With this code instead, it feels reasonable:
x = 3
y =
x = x + 1
x
# this prints 3
dbg x
That's why I suggested not allowing that ;)
If the thing you're updating is from an outer scope, the results would definitely be non-intuitive, particularly for people coming from procedural languages. At the very least, that needs to cause a warning.
personally I think that concern applies about the same amount to both examples
e.g. I've already seen in advent of code someone try to reassign something declared in an outer scope using x = ...
I think the proportion of beginners who would understand how = works (in the shadowing world) but think += works like it does in imperative languages would be small enough not to be worth worrying about
personally I think that concern applies about the same amount to both examples
Yeah that's fair. But I would still tend to start conservative in what we allow. Easier to open it up later than it would be to lock it down.
Richard Feldman said:
I think the proportion of beginners who would understand how
=works (in the shadowing world) but think+=works like it does in imperative languages would be small enough not to be worth worrying about
Really? That assumption surprises me. I would guess many more new users would hit issues like seen in AOC with += than with shadowing.
I'm not a new user and I still have trouble reading the += example even though I know what it means and how it works. I am simply way too used to += being inplace.
I think that'll fade though. I had a sketch with this syntax from earlier and I got used to it :big_smile:
basically my prediction is that if people can look at this and know that it isn't reassigning x:
x = 3
y =
List.map nums \num ->
x = x + num
...then I think they can also realize that this isn't either:
x = 3
y =
List.map nums \num ->
x += num
in part because if my mental model is "x += num is sugar for x = x + num" and I know what x = x + num means then I can pretty quickly know that += plays by the same rules as =
I think this discussion might have strayed somewhat from the original proposal, and I'm not sure I'm really following here. Are we talking about just adding syntax for within a record? or more generally adding new operators along with shadowing?
Richard Feldman said:
{ nonempty, elems } = List.walk lists { nonempty: 0, elems: [] } \state, list -> { state & nonempty: + if List.isEmpty list then 0 else 1, elems: |> List.concat list }
I don't think I like this syntax. Maybe it will grow on me, but I think it looks a little strange/confusing. Mostly just the + operator. I like the pipe though...
We have the record update syntax that just landed, so you could write the above like this now
{ nonempty, elems } =
List.walk lists { nonempty: 0, elems: [] } \state, list ->
state
|> &nonempty (if List.isEmpty list then state.nonempty + 0 else state.nonempty + 1)
|> &elems (List.concat state.elems list)
I'm guessing one of the goals here is to make the syntax more approachable or familiar to programmers who come from imperative/procedural backgrounds? reduce the weirdness budget etc.
One benefit of having the syntax structured like a record, is that it is clearer that it is producing a record.
personal
Luke Boswell said:
I'm guessing one of the goals here is to make the syntax more approachable or familiar to programmers who come from imperative/procedural backgrounds? reduce the weirdness budget etc.
kinda, although I'd say the main thing for me is trying to make the equivalent of for loops nicer in Roc
today if you compare code that uses walk vs an imperative implementation that uses for, there's a lot more repetition in the walk version
So if we allowed += in record fields and record field assignment (technically update + shadowing), the original example would become:
{ nonempty, elems } =
List.walk lists { nonempty: 0, elems: [] } \state, list ->
state.nonempty += if List.isEmpty list then 0 else 1
state.elems = List.concat state.elems list
state
I definitely prefer this over most of the syntaxes suggested above.
I think the _ as filler or being able to uses
.field in a record update as the actual field value instead of an accessor are the only two that are close.
To be concrete. Underscore syntax (this is overloaded with throw away variables, which might be ok):
{ nonempty, elems } =
List.walk lists { nonempty: 0, elems: [] } \state, list ->
{ state &
nonempty: _ + if List.isEmpty list then 0 else 1,
elems: List.concat _ list
}
And .field syntax (course this is overloaded with field access and this exact syntax probably shouldn't be used):
{ nonempty, elems } =
List.walk lists { nonempty: 0, elems: [] } \state, list ->
{ state &
nonempty: .nonempty + if List.isEmpty list then 0 else 1,
elems: List.concat .elems list
}
I don't like any of the other suggestions so far.
Also, in roc today, this could be written cleaner as:
(nonempty, elems) =
List.walk lists (0, []) \(nonempty, elem), list ->
(
nonempty + if List.isEmpty list then 0 else 1,
List.concat elems list
)
At least for the specific case of state for walk, this may be the best bet. Concise without any magic sugar. Short enough any users can still understand it nonetheless. Also, state of walk is likely to be fully rewritten on every iteration, so fully dropping record update syntax is no big deal.
Honestly, maybe for most uses of walk (short without that much state), tuples are the solution.
I’d say it’s also true for records. This way you don't have to write state.
nextState = state |> \{ x, y } -> {
x: x + y,
y: y + 1,
}
Yeah, could be done with a record and destructoring then building a new record. More noise though.
{ nonempty, elems } =
List.walk lists { nonempty: 0, elems: [] } \{nonempty, elem}, list ->
{
nonempty: nonempty + if List.isEmpty list then 0 else 1,
elems: List.concat elems list
}
If we consider getters, is it possible to expand the idea to a common case? Like,
transform = \{.}, a -> .x + .y + a
Which is
transform = \state, a -> state.x + state.y + a
We can call {.} “curly nipple”
I don't think that has much value over simply destructoring
As you said, destructuring might be a bit noisy
In a walk, I don't think this gains anything compare to just using a tuple. Cause walk requires building a new version of the record which will still repeat all the names
For a simple walk, sure. But I can imagine more complex walks where there are a bunch of different branches, and on each of those branches a small subset of the full state actually needs to be updated and you're just repeating fields for the rest of the state. e.g. a bytecode interpreter loop
I don't think this is really about complex walk. It is about the common case.
Complex walk is really a different question. It is more a question about general large function ergonomics
More general
This might be pie-in-the-sky thinking given it's type-level and opportunistic mutation implications, but if we ever consider supporting rest-patterns in record constructing and deconstructing, I think that by itself would also allow record updates to be written in a nicer way:
updateState = \{ nonempty, elems, ...state } ->
{
...state,
nonempty: nonempty + if List.isEmpty list then 0 else 1,
elems: List.concat elems list
}
Another random, possibly bad thought: What if in record-update syntax each field assignment always takes a lambda?
{ state &
nonempty: \prev -> prev + if List.isEmpty list then 0 else 1,
elems: \prev -> List.concat prev list
}
If the new value does not depend on the old you'd need to: { old & field: \_ -> 42 }. That's worse than the current syntax, but:
I'm wondering how often it happens when updating a record field that the new value does not depend on the old. Can't come up with good examples. It might be the exceptional case?
I think this misses the relatively common case of complex updates.
If the update is too complex, you don't want to do it inline:
somePrecursorWork = ... using state.nonempty
nextNonEmpty = ... using somePrecursorWork
{ state &
nonempty: \_ -> nextNonEmpty,
elems: \prev -> List.concat prev list
}
It is also semi common for field updates to depend on multiple values. If that happens, prev doesn't work. elems might depend on both elems and nonempty to calculate the next value. I think this is also relatively common
I think we do theoretically have a syntax for this, but I don't think it works
updateState = \{ nonempty, elems, ...state } ->
{
...state,
nonempty: nonempty + if List.isEmpty list then 0 else 1,
elems: List.concat elems list
}
I think this was supposed to work (or something similar)
updateState = \{ nonempty, elems } as state ->
{
state &
nonempty: nonempty + if List.isEmpty list then 0 else 1,
elems: List.concat elems list
}
That said, :point_up: does not work today (but maybe I just have the syntax wrong).
Richard Feldman said:
a related idea I had was:
{ state & nonempty: if !List.isEmpty list then state.nonempty + 1 }so "if without else" and then the implied else branch is "whatever the field currently is"
I often find myself wanting a "no else branch", like they have in F#, when using html boolean attributes in roc-html.
input [type "checkbox", if message.selected then Attribute.checked else (Attribute.attribute "") ""]
# vs
input [type "checkbox", if message.selected then Attribute.checked]
that's interesting!
Yeah, I wanted that in Elm so many times :grinning:
same here haha
the alternative looks awkward and is worse for perf
Jasper Woudenberg said:
Another random, possibly bad thought: What if in record-update syntax each field assignment always takes a lambda?
{ state & nonempty: \prev -> prev + if List.isEmpty list then 0 else 1, elems: \prev -> List.concat prev list }If the new value does not depend on the old you'd need to:
{ old & field: \_ -> 42 }. That's worse than the current syntax
How about a special lambda syntax which is only allowed in that position? Non-depedent values don't require a lambda
{ state &
nonempty: ~\prev -> prev + if List.isEmpty list then 0 else 1,
field: 42
}
Just throwing it out there :smile: (relates to #ideas > new dot-dot syntax for list interpolation, open types, etc.)
{
..state,
..if List.isEmpty list then
{ nonempty: state.nonempty + 1 },
elems: List.concat elems list
}
Richard Feldman said:
basically my prediction is that if people can look at this and know that it isn't reassigning
x:x = 3 y = List.map nums \num -> x = x + numWait... so what is this doing? Not reassigning, sure, but how is the left-hand-side x used, or what effect does it have?
It certainly looks like the outer x is being assigned to a newly declared inner, shadowing x, yet that inner x appears to be unused
In this case it is doing nothing cause it is a silly example. Probably should at a minimum be this:
x = 3
y =
List.map nums \num ->
x = x + num
x
vs:
x = 3
y =
List.map nums \num ->
x += num
x
Either way it is bad example code, but at least these would both be theoretically valid
This example suggests to me that maybe we should only allow shadowing x in the same block and at the same indentation level as where it is defined
The example is bad, but there are plenty of good examples of shadowing in a different block or indentation level.
The simplest one being conditionals
I had conditionals in mind when I said that. For example, this program printing 5 is probably not intended.
main =
x = 5
if x < 42 then
x = 42
Stdout.line! "Too low!"
else
Stdout.line! "Just right.."
Stdout.line! (Num.toStr x)
I think we are kinda on a tangent of a tangent at this point
Shadowing is coming to roc at least to try out. It has been discussed significantly in other threads including examples like this. We can discuss more specifics about it but that should really get pulled into its own thread.
The discussions here was specifically: since we will have shadowing, should we always inplace update syntax like x += 1.
This is a tangent on the original discussion of record update syntax where it was realized that will shadowing and inplace update syntax on records, you could remove record update syntax all together:
x.field += 3 or x.field = 32 would be equivalent to using current shadowing and record update syntax.
Just trying to tie this back to the original discussion here
Apologies in advance if this is a bit rough, I've never written any Roc code, just read the Roc tutorial a couple of weeks back when I found it (from Koka) and can't get it out of my head. :)
It would be really nice if record field updates could take an expression or a function:
updateZ: Int -> Int = \z -> if isOdd z then z + 1 else z
nextState =
{
state &
x: state.x + 1,
y: if state.y > 0 then state.y - 1 else state.y,
z: updateZ,
time: \prevTime -> prevTime + delta,
}
If there was extra syntactic sugar for something like a Nothing where returning a Nothing means don't update the current field value and where if x then y desugars to a -> [b, Nothing] = \x -> if x then y else Nothing then we could also have this:
updateZ: Int -> [Int, Nothing] = \z -> if isOdd z then z + 1
nextState =
{
state &
x: state.x + 1,
y: if state.y > 0 then state.y - 1,
z: updateZ,
time: \prevTime -> prevTime + delta,
}
And if there was also Scala-like syntactic sugar for shorthand lambdas (in some nice-ish way):
updateZ: Int -> [Int, Nothing] = \z -> if isOdd z then z + 1
nextState =
{
state &
x: _ + 1,
y: \y -> if y > 0 then y - 1,
z: updateZ,
time: _ + delta,
}
Last updated: Jun 16 2026 at 16:19 UTC