Stream: ideas

Topic: reddit record update syntax


view this post on Zulip Anton (Apr 17 2023 at 12:31):

Another syntax suggestion
"""
This was a bit weird:

{ record & x = 3, y = 4 }

My brain's expression parser thinks record & x = 3 is one part and y = 4 is the other. I would've preferred:

record & { x = 3, y = 4 }

"""
"""
Or keep Rust's syntax, to be consistent with type constraints.

{ x = 3, y = 4, ..record }
{ ..record, x = 3, y = 4 }

Not sure about that first one; the compiler should probably enforce the second one to make it clear x and y overwrite the old values in record. My vote is on some sort of record1 & record2 operator.
"""

view this post on Zulip Richard Feldman (Apr 17 2023 at 12:49):

supporting record1 & record2 would require a major type system change, not just syntax

view this post on Zulip Richard Feldman (Apr 17 2023 at 12:51):

I'm open to something like the { ..record, x = 3, y = 4 } idea, but a natural question that might arise is "why can't I do { ..record1, ..record2, x = 3, y = 4, ..record3 } like I can in JavaScript?"

view this post on Zulip Richard Feldman (Apr 17 2023 at 12:51):

and maybe that's a plausible idea, but now again it's more than just a syntax change

view this post on Zulip Anton (Apr 17 2023 at 13:18):

"why can't I do { ..record1, ..record2, x = 3, y = 4, ..record3 } like I can in JavaScript?"

we can leave that for later :p

view this post on Zulip Richard Feldman (Apr 17 2023 at 13:21):

well but given that it's a predictable beginner question, I'd want to be careful not to trade one beginner question for a different one :big_smile:

view this post on Zulip Richard Feldman (Apr 17 2023 at 13:21):

(not saying it's a bad idea or anything, just that if it turns out we don't actually want to support all of that, we should probably figure that out before changing it!)

view this post on Zulip Richard Feldman (Apr 17 2023 at 13:22):

for context, I actually originally had the syntax being something like that, including the flexibility to mix and match, but then when @Folkert de Vries and I were pairing on it (back in 2019) it turned out to be a lot harder to implement than anticipated

view this post on Zulip Richard Feldman (Apr 17 2023 at 13:22):

so I said "ok let's just do what Elm does after all" - with the one change of using & instead of | because | has a different meaning in Roc (e.g. | patterns)

view this post on Zulip Richard Feldman (Apr 17 2023 at 13:25):

and that's what the syntax has been ever since!

view this post on Zulip Richard Feldman (Apr 17 2023 at 13:25):

so I'm totally open to changing it, just while being mindful of the can of worms haha

view this post on Zulip Brendan Hansknecht (Apr 17 2023 at 14:53):

What is the technical complication of record1 & record2. If we say & is only for updating. record2 must have the same or a subset of the fields of record1. The types of each field must match. Then we just copy over the data.

view this post on Zulip Brendan Hansknecht (Apr 17 2023 at 14:54):

I would assume the main complication would be for users when they expect it to be very flexible (like in js), but it only works in a very limited use case.

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

suppose I put this into the repl:

\a, b -> a & b

what is the inferred type of that function?

view this post on Zulip Luke Boswell (Apr 17 2023 at 19:49):

What about a middle ground { ..record, x: 3, y: 4 }?

I like how this still uses a comma, and implies that it takes the fields from record. I guess it might be strange that x could be overwriting the value in record maybe?

view this post on Zulip Georges Boris (Apr 17 2023 at 21:29):

yeah... the thing with that is that people will assume the spread syntax can also be used the other way around { ...first, x: 1, ...second } then the response becomes the union of all records? anyway, if this was feasible technically then I think it would be a good api. if it's not but someday will be, then it could still be a good api since we could catch the unsupported usages and explain it properly in the compiler error.

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

yeah that's what I think people would expect :point_up:

view this post on Zulip Richard Feldman (Apr 17 2023 at 22:16):

and I think it's a reasonable option, but I do think we should first investigate what mixing and matching where the ... goes would look like from a type-checking perspective, because if we end up deciding to implement and then immediately get asked about it, I'd rather know the answer before we even started implementing it :big_smile:

view this post on Zulip Ayaz Hafiz (Apr 17 2023 at 23:23):

I am not worried about \record1, record2 -> record1 & record2for type inference PoV - the inferred type of this is {}a, {}a -> {}a, which is all well

view this post on Zulip Ayaz Hafiz (Apr 17 2023 at 23:26):

However I do think that this makes the semantics look like this would perform a record intersection, which it would not, unless we also expanded the runtime semantics to allow arbitrary growing of records (which has its own set of problems, namely that it incurs a runtime cost that might be small, but may also be non-obvious). The current record update semantics are only for updates - they can't grow the record, but a syntax like F & G, especially to those familiar with TypeScript, may be unexpected

view this post on Zulip Richard Feldman (Apr 17 2023 at 23:55):

Ayaz Hafiz said:

I am not worried about \record1, record2 -> record1 & record2for type inference PoV - the inferred type of this is {}a, {}a -> {}a, which is all well

I don't think that's the type people would expect it to infer though!

view this post on Zulip Richard Feldman (Apr 17 2023 at 23:55):

that type says both records have exactly the same set of fields

view this post on Zulip Richard Feldman (Apr 17 2023 at 23:56):

so I couldn't pass it { x: 0, y: 0, z: 0 } and { x: 1, y: 1 } because it would say I'm missing the z field

view this post on Zulip Richard Feldman (Apr 17 2023 at 23:57):

but I would expect that if I can do { x: 0, y: 0, z: 0 } & { x: 1, y: 1 } then I could also extract out named variables for both of those and have it still work

view this post on Zulip Richard Feldman (Apr 17 2023 at 23:57):

or am I missing something and that would actually work even though the extension variables are the same? :thinking:

view this post on Zulip Richard Feldman (Apr 17 2023 at 23:59):

Ayaz Hafiz said:

I do think that this makes the semantics look like this would perform a record intersection, which it would not, unless we also expanded the runtime semantics to allow arbitrary growing of records (which has its own set of problems, namely that it incurs a runtime cost that might be small, but may also be non-obvious). The current record update semantics are only for updates - they can't grow the record, but a syntax like F & G, especially to those familiar with TypeScript, may be unexpected

I'm also open to this change, even though there would be a runtime cost sometimes (which might be surprising and potentially significant) but yeah, that's a separate discussion I think :big_smile:

view this post on Zulip Ayaz Hafiz (Apr 18 2023 at 00:28):

I’m not sure how “{ x: 0, y: 0, z: 0 } & { x: 1, y: 1 }” would work either? Unless the proposal here is that there is a new syntax solely in the record updating case with a record literal. Because then you could not even do

r1 = { x: 1, y: 1 }
{ x: 0, y: 0, z: 0 } & r1

with today’s semantics of Roc - this is not admissible.

view this post on Zulip Richard Feldman (Apr 18 2023 at 00:36):

right, but that's a core use case for record updates right?

view this post on Zulip Richard Feldman (Apr 18 2023 at 00:37):

like { xyzRecord & x: 1, y: 1 }

view this post on Zulip Richard Feldman (Apr 18 2023 at 00:37):

so if infix & can't do that, then I don't think it would be very nice to use :big_smile:

view this post on Zulip Ayaz Hafiz (Apr 18 2023 at 00:42):

The infix case is different in that the syntax is restricted though. You can’t put any arbitrary record there (syntactically) - you can syntactically only list certain fields that are eligible to update.

view this post on Zulip Ayaz Hafiz (Apr 18 2023 at 00:42):

If the same restriction is applied in this proposal there is no worry from a typechecking perspective right

view this post on Zulip Richard Feldman (Apr 18 2023 at 00:43):

sorry, when I say infix & I mean record1 & record2

view this post on Zulip Richard Feldman (Apr 18 2023 at 00:43):

so not syntax-restricted

view this post on Zulip Richard Feldman (Apr 18 2023 at 00:44):

definitely { xyzRecord & x: 1, y: 1 } does not have this problem, but I think \record1, record2 -> record1 & record2 having the type {}a, {}a -> {}a would have this problem

view this post on Zulip Ayaz Hafiz (Apr 18 2023 at 00:45):

well, if the goal is to arbitrarily update records with any fields, I do not see how we get there without allowing the runtime to change records to augment additional fields :joy:

view this post on Zulip Ayaz Hafiz (Apr 18 2023 at 00:45):

do you think there is another way?

view this post on Zulip Richard Feldman (Apr 18 2023 at 00:46):

oh I agree - I assume if we wanted to allow record1 & record2 we'd have to allow the runtime to add fields

view this post on Zulip Richard Feldman (Apr 18 2023 at 00:46):

but my original point was that record1 & record2 couldn't be just a syntactic change, but rather would have to be a type system change too (and code gen)

view this post on Zulip Ayaz Hafiz (Apr 18 2023 at 00:49):

with the specific semantics of wanting to arbitrarily extend records I agree. with the current semantics it could be made to work though even if it’s maybe not the most intuitive behavior which i think we also agree on.

view this post on Zulip Brendan Hansknecht (Apr 18 2023 at 02:46):

I still think you could technically make record1 & record2 restrict record2 such that it is the same fields or a subset of the fields of record1. Then you can update, but it can never grow.

view this post on Zulip Brendan Hansknecht (Apr 18 2023 at 02:47):

That said, I do think that would be a terrible confusing interface especially with context from other languages, I just think it is technically possible.

view this post on Zulip Anton (Apr 18 2023 at 08:49):

I'm starting to think neither the current syntax nor any new proposals are good because none are self-evident to newcomers.

view this post on Zulip Anton (Apr 18 2023 at 08:49):

I propose:

Record.update coords { x = 3, y = 4 }

more verbose, but also self-evident, simple and familiar

view this post on Zulip Anton (Apr 18 2023 at 09:28):

Although I don't think we can express the type signature for that currently, which makes me wonder how the compiler handles our current record updates

view this post on Zulip Agus Zubiaga (Apr 18 2023 at 10:14):

I think that’d be typed the same as (&) if it were to be introduced, it’s just the non-infix version

view this post on Zulip Andrei Vasiliu (Apr 18 2023 at 13:28):

Is it possible to spell out the type of Record.update? Or is this (&) something that already exists and has a special type?

view this post on Zulip Anton (Apr 18 2023 at 13:38):

Is it possible to spell out the type of Record.update?

Not currently, no.

Or is this (&) something that already exists

The (&) mentioned by Agus refers to record1 & record2, which is not supported now. If we want to implement it, I expect it would be an infix notation that would desugar to Record.update under the hood.
Right now we support { user & email: "blah" }, but I don't know how that is handled in the compiler.

view this post on Zulip Brendan Hansknecht (Apr 18 2023 at 15:33):

I think record update is fundamental to a number of applications, so i don't really prefer it as explicit, but i totally see the argument.

I think record update is a simple enough syntax that you just need to learn once.

view this post on Zulip Brendan Hansknecht (Apr 18 2023 at 15:36):

Also, if you make it a function, how do you stop:
Record.update record1 record2 and all of the complications that it leads to? Discussed above as issues with record1 & record2. I think that would be even more confusing to end users. This function takes 2 records, but i have to define the second record inline or it errors out. I think that makes more sense to error out in a custom syntax like we have with &.

view this post on Zulip Anton (Apr 18 2023 at 15:38):

Yeah, I agree

view this post on Zulip Georges Boris (Apr 18 2023 at 15:40):

on elixir, we have both explicit and non-explicit record updates and we basically never use the non-explicit ones since we can't compose then as part of pipes, etc.

we also have an utility for nested updates but trying to think about it a way that makes sense with types is a bit weird... so I wouldn't expect it to be possible in Roc.

view this post on Zulip Georges Boris (Apr 18 2023 at 15:42):

Record.update : { ..a, ..b } -> { ..a } -> { ..a, ..b }

Seems like a pretty nice type signature for things like the phantom builder pattern :sweat_smile:

view this post on Zulip Brendan Hansknecht (Apr 18 2023 at 15:43):

Elixir is dynamic and allows for record update to add fields?

view this post on Zulip Georges Boris (Apr 18 2023 at 15:47):

the non-explicit syntax doesn't allow that. the field needs to be something that is already in the map. I don't know the reason. but yeah - the explicit allows new fields to be added. structs are just maps in elixir.

I was only talking about the usefulness of something like Elm's update syntax vs an explicit function. The explicit function might provide better ergonomics in the end... but I understand the problem related to type signature.


Last updated: Jun 16 2026 at 16:19 UTC