As part of the new .. syntax proposal, the new means to show an open record type alias is User a : { name : Str, age : U64, ..a }, where a is a separate record type with additional fields for User.
One of the proposed syntax additions is to allow the same syntax when capturing record fields, a.k.a { name, age, ..rest } =, where rest is a new record holding the other fields besides name and age. This would require us to create another struct type, and copy the other fields in the destructured record to this new struct.
Another proposal is to allow extending records using a spread operator, a.k.a userWithEmail = { ..user, email: "..." }. This would also require defining a new struct, copying all of user's fields over, and then setting the new email field, all at runtime.
Between these two operations, we'd get more flexibility for creating records, but would promote a potentially slow operation at runtime. Thoughts an opinions on the support of one or both of these features below!
I'm definitely not a fan of this. While there are some cases it would be really convenient to avoid boilerplate, I think it is a big performance antipattern. Especially given the larger the struct you do this to (which is where this is the most convenient), is where the most performance is lost.
I also think a common pitfall that new users run into is trying to use records as dictionaries. I think this would make it possible to kinda do that in a super terrible way.
I agree, and I think by allowing either { a, b, ..rest } = or { a, b, .. } as rest = to set rest as the full record would make the same code possible without runtime overhead.
When I say "the same code is possible", I mean that if rest has the fields a and b included, it doesn't need to use them, but it at least has all other fields included, so you can still use rest.otherField.
That's actually why I'd lean away from { ..rest } and towards { .. } as rest, because it doesn't imply that a and b are dropped
As I mentioned In the original thread. Generally if you want to do something like this there are better alternatives that balance perf and usability.
For {name, age, ..rest} = ..., generally you can just keep around the full record and still reference fields as needed {name, age } as rest = ..... we even allow for a record to be passed to a function that only knows about a subset of the fields due to open records. So other alternative with good perf and reasonable ergonomics exist for most uses of this.
For userWithEmail = { ..user, email: "..." }, This will definitely lead to users thinking records work as dictionaries and being frustrated when they are limited. Instead of doing this, a user can use nested records or have a record generate that is originally big enough to hold the email. If a field is know to be needed it should probably exist from the get go.
Tons of statically typed languages do just fine without these kinds of features. I really don't think we should add them. They are bad for perf and become worse for perf the more convenient they are to use.
On alternative I think might be valid that is more explicit. Allow this spread operator with record update syntax. If a user really wants to add a field, they can define a new full sized record (maybe with all default fields) then update every field from the sub record. I think this is much more clear about the cost of adding a field and it is much more optin on than accidental. It also would work with open records (which is a good thing).
userWithEmail = { defaultUserWithEmail & ..user, email: "..." }
Note this alternative is essentially how rust enables something like this.
I think we should just change the record update syntax from { existing & foo: bar } to { foo: bar, ..existing } for consistency with other .. syntaxes, as well as being more obvious for JavaScript and Rust devs
Oh, but that would actually break my alternative potentially
{ email: "...', ..user, ..defaultUserWithEmail }
Brendan Hansknecht said:
I'm definitely not a fan of this. While there are some cases it would be really convenient to avoid boilerplate, I think it is a big performance antipattern. Especially given the larger the struct you do this to (which is where this is the most convenient), is where the most performance is lost.
I also think a common pitfall that new users run into is trying to use records as dictionaries. I think this would make it possible to kinda do that in a super terrible way.
I kinda like the idea of parsing it and then giving an error that explains the perf cost and why you should use as instead
like "hey this would actually create a whole new record; if that's what you want, just make a new record from scratch so it's clear that's what's happening"
That's a great idea!
Yeah, I like that!
Sam Mohr schrieb:
I think we should just change the record update syntax from
{ existing & foo: bar }to{ foo: bar, ..existing }for consistency with other .. syntaxes, as well as being more obvious for JavaScript and Rust devs
I think this would actually be inconsistent with other .. syntaxes because the new .. is usually a spread, so ..existing says "put all of existing here."
So if existing = { foo: 1, bar: 2 } and you put { ..existing, foo: 3 }, it reads like you're assigning foo twice, as in { foo: 1, bar: 2, foo: 3 }, which isn't allowed.
Even if the syntax for adding/removing fields never makes it into the language, it would be confusing to have the same syntax but with a different meaning.
{ ..{ foo: 1, bar: 2 }, foo: 3 } gives you { bar: 2 , foo: 3 } because foo: 3 is the last foo assignment (reading left to right) so it has the last say.
This is how it is implemented in JS and is not really confusing IMO
Yeah, putting at the end as I did is wrong, yes
If we do it at the beginning, we're good, as described above
requiring an order feels pretty strange to me cause records aren't ordered. It also feels very different from a spread which is additive in lists. So I'm not a fan
Actually, you're right, lists can be added onto at the end, this would work the same
We seem to have just leaned towards allowing { x, y, ..rest } = full_record to extract the remaining fields besides x and y from full_record into rest, in good part because it shouldn't affect runtime performance or compiler performance in any noticeable way.
Should we reconsider allowing this? Would it significantly worsen runtime or compiler performance with repeated usage?
My gut feeling at this point is that:
An important note: Any large record is already being past around as const ref. As such, any record update on it will create a copy of all of the data it contains. So this syntax will do the same, just with a relayout as well (which could add to the expense but is probably fine). Also for anything purely local (or inlined), llvm will optimize it to be nice anyway.
How is this different than { foo & x, y }?
currently {foo & x, y} requires foo to contain an x and a y field
Or am I imaging that syntax?
Ok
This allows for the new record grow to contain more fields
Interesting
Oh, and the syntax is just swapping over everything to use .. and is removing the current record update syntax.
So I get some record type an and then return a {x, y}a?
They would at least be consistent
I like it
But I also program mostly JavaScript so I would
Hmm...does this have a type issue:
fn : { ..rest } -> { a: Str, ..rest }
fn = |record|
{ ..record, a: "yes" }
Does this make sense as a type? It seems to suggest that rest doesn't contain a, but it may or may not contain an a.
I think that's how it'd work
There's a chance this could replace record builders...
I think that would mean we need a different system of type variable unification. Generally the type variable here would just be adding fields. Now they need to not only add fields but also need to handle the case of a record update that might replace a field
It wouldn't look like you're building a record with these
Later, I'll try making another Weaver demo of what I mean
Sure, but I don't think this type works: fn : { ..rest } -> { a: Str, ..rest }
Yep, you're right
That was an aside
Well, I'm not sure
So either we need a new type syntax to deal with record updates or we need to make types for spreads to be smarter to deal with record updates either adding or mutating a field.
To be fair, we could just make the syntax mirror the code: fn : { ..rest } -> { ..rest, a: Str }. Would require a different unification algorithm, but should be sound
Actually, couldn't that work with fn : rest -> { ..rest, a : Str }?
Which still requires a type system change
Since you need to know the relationship between rest and ..rest's usage
ah yeah
don't need the extra ..
i would be surprised if this doesn’t already work
rest -> {a: Str}rest
on mobile sorry for formatting. if it doesn’t that’s a bug
Yeah, we're good:
module []
extend_with_a : data -> { a : Str }data
extend_with_b : data -> { b : Str }data
expect
{}
|> extend_with_a
|> extend_with_b
== { a: "abc", b: "def" }
This typechecks
sick
That isn't enough. For it to also fulfill record update syntax, this has to work too:
module []
extend_with_a : data -> { a : Str }data
extend_with_b : data -> { b : Str }data
expect
{ a: "test" }
|> extend_with_a
|> extend_with_b
== { a: "abc", b: "def" }
This suggested change would both allow addition and updating an existing field
I would think that example would work today, but what I think is the more bizarre case that someone _may_ want to try is:
module []
extend_with_a : data -> { a : Str }data
extend_with_a = |data| {
a: "abc",
..data,
}
extend_with_b : data -> { b : Str }data
extend_with_b = |data| {
b: "def",
..data,
}
expect
{ a: 123 }
|> extend_with_a
|> extend_with_b
== { a: "abc", b: "def" }
That should be a type error IMO
I would hope so
Yeah, I think that would always be a type error
Last updated: Jun 16 2026 at 16:19 UTC