Hi there. I have been porting some old Roc code I have but I don't seem to be able to get record extension to work. Here is a small example of what I would expect to work, but the error is clearly telling me this is not what I want:
main! = |_| {
r = { a: 1 }
_ = { ..r, f: 0 }
Ok({})
}
➜ roc git:(main) ✗ roc main.roc
TYPE MISMATCH - This record does not have a f field.
This is often due to a typo. The most similar fields are:
- a
So maybe f should be a?
Note: You cannot add new fields to a record with the record update syntax.
Found 1 error(s) and 0 warning(s) for .../main.roc.
Is this something that is supposed to be supported?
Hi @JRI98,
We have this code in all_syntax_test.roc:
record_update_2 : { name : Str, age : I64 } -> { name : Str, age : I64 }
record_update_2 = |person| {
{ ..person, age: 31 }
}
So looks like that failure could be a bug.
But in that case the age field is already part of the type. In my case, I'm trying to create a record from an existing one and add additional fields.
Here is another related case that I'm not understanding:
f : { ..a } -> { f: U64, ..a }
f = |r| {
{ ..r, f: 0 }
}
main! = |_| {
Ok({})
}
TYPE MISMATCH - The r record does not have a f field.
It is actually a record with no fields.
It could be the case that it doesn't work like I want it to :sweat_smile:
I think this may be a performance foot gun if people start using it liberally so that could be why it is not supported.
It could lead to a lot of data needing to be copied to a new place in memory which could become problematic with large records and lists of records.
I understand that. Maybe there is a way to be explicit about wanting that behavior?
Because otherwise I don't see how I can extend a record I don't know the full shape of.
Perhaps we could make a Record builtin function append_slow?
i’m not sure this is allowed — in the zig type checked we based thing off of elm, and in elm you cannot use record update to add fields. i don’t recall if in the rust compiler we loosened up that restriction
but in either case, we need to make a change:
i’m not sure the pros/cons of allowing it (outside the perf point above).
as a short term fix, you should be able to create a new record manually writing out the fields to copy from the old, then adding the new fields
oh wait, well the first error does say “you cannot add new fields with the record update syntax”, but the second error is confusing
maybe relevant elm topic: https://discourse.elm-lang.org/t/extending-an-existing-record/5330/2
as a short term fix, you should be able to create a new record manually writing out the fields to copy from the old, then adding the new fields
That's the thing, in the second case I don't know which fields there are.
oh wait, well the first error does say “you cannot add new fields with the record update syntax”, but the second error is confusing
Yeah that error message was what led me to open this thread. I figured I was trying to do something that wasn't supported.
JRI98 said:
as a short term fix, you should be able to create a new record manually writing out the fields to copy from the old, then adding the new fields
That's the thing, in the second case I don't know which fields there are.
Just to make sure, you mean you don't know, but you could know if you were to inspect the code statically, right? So in your example, if you were to call f with a struct, you'd know what fields are in it. I'm asking because while this is doable (i think), javascript-style spread operator is not, since they are working on dynamic collections, whereas the compiler needs to know the size of every struct in Roc at compile time, so it needs to know all the fields that go into a struct.
If my intuition is correct, you cannot even have proper code with an entrypoint (from "main" or an expect) that calls f where you statically don't know what's in the extended struct. I don't know of another language that has this feature for unboxed, C-style structs, which Roc structs are, so I think the need for it arises as the naturally feeling next step after assembling a structurally typed record with this syntax.
Maybe the extension is the problem in the name and it should be more like record composition / field composition / field inference. Or I think it was called record update sytax originally. I don't know whether I've helped with the matter. Getting late and I'm rambling, so I'm going to go now, have a good one!
When I say "I don't know which fields there are" I mean myself in the context of the example above where I want a function to add a field to a generic record. But the compiler could always infer it statically.
yeah I need to dig into the perf to figure out if it's actually a footgun, and if so how much :smile:
in the original paper that Elm's extensible records are based on, it supported:
foo, and then remove 2 of them, and be back to just the original foo)Evan had all of those features implemented in...I want to say Elm 0.12? But they turned out to be performance footguns and/or more complicated/nonobvious than useful, so he took them out in a later release
and that's where we started
the copying concern _could_ be a footgun, but I'm not sure if it actually would be in practice depending on how it would be used. Like if we're talking about a small record on the stack, it might end up being zero because LLVM breaks it down into registers anyway
also, if you have a bigger record that you're going to be copying it into regardless, and it would just be a convenience for what you'd do anyway, then it's also zero cost
the concern is more like "I used this language feature to implement something that wouldn't otherwise be something anyone would try, and the performance is terrible"
so the specific thing I'd like to investigate is how this is used in the TypeScript world
because "I used this in that ecosystem and I liked it" would be a likely way that something would end up in Roc, except that of course people have higher perf expectations of Roc than TypeScript :smile:
a concrete example of something I'd be concerned about would be database queries - are these used for building those up? if so, how does that look? would it be a perf footgun if we did it in Roc? etc.
so it's definitely doable on a technical level, but I don't want to add a feature to the language where the langref would be like "hey this exists, and it's sometimes a convenience but other times blows up your performance, so probably best to avoid it"
Is this something that a more performant runtime thing like a Dict is more suitable?
I know the syntax isn't as ergonomic... but is it somewhat related here?
I think they have a different goal. You'd use this capability to 'add a field to an arbitrary struct'.
Suppose you're writing an lsp library. You accept a struct from the user which is the capailities of his lang server, that you indted to just serialize and send off. But because your library doesn't support hover, you want to make sure that the client knows this. So you update the capabilities struct's hoverProvider field to be false and leave the rest. You as the library author wouldn't know what's in the struct, it may not even have a hoverProvider field, in which case you didn't update it, you added it in. This way people could use structural records more freely and I'd argue in a way that's very fitting for structurally typed records. If the perf isn't a problem, I'd use this feature for even the most basic things, like 'I have a struct of x,y coordinates. You run them through this function and they become 3d coordinates'. I'd want to write it as
new_coords = {
..2d_coord,
z: 0
}
i skimmed through this and I think the relevant optimization here is scalar replacement of aggregates. The compiler should disassemble all structs stored in local variables into the scalar values they are made up of, then shuffling these scalars becomes a noop and the compiler only actually materializes the full struct once it is passed to a function or written to memory. It never needs to actually shuffle values between structs before that, even if the programmer wrote it like that.
Last updated: Jul 23 2026 at 13:15 UTC