Stream: ideas

Topic: Non-complete records syntax


view this post on Zulip Kiryl Dziamura (Mar 03 2024 at 15:27):

I am very cautious and suspicious of any kind of defaults: it's hard to follow the call-side code, especially if you’re not familiar with the codebase and/or you explore the code with no IDE features (github/code review). It implicitly adds an argument/configuration field so it's easy to miss - you have to be aware of the function interface ahead of time.
What do you think of having a special syntax for non-complete records in Roc? Smth like { .. & x: 1}?

f = \{ x, y ? 1 } -> x + y
f { .. & x: 1 }

This may have been discussed before, but I couldn't find the thread quickly.

view this post on Zulip Kiryl Dziamura (Mar 06 2024 at 20:14):

Any thoughts?

view this post on Zulip Richard Feldman (Mar 06 2024 at 21:03):

I could see something more concise, like maybe { x : 1, .. }

view this post on Zulip Norbert Hajagos (Mar 07 2024 at 07:00):

I think this would be a great addition if we didn't use records as a means to named and default arguments. I wouldn't mind calling the functions that I or my collegues wrote that way, especially if this is a record with config options. The benefit you talked about would be there. For builtins like List.range though, I wouldn't want the syntax to be even more verbose, because the added value would be small.

view this post on Zulip Kiryl Dziamura (Mar 07 2024 at 08:33):

more concise, like maybe { x : 1, .. }

To be fair, it saves one space :grinning: but I agree. Plus, it's always in the end so you won't have weird { a & .. & b }

For builtins like List.range though, I wouldn't want the syntax to be even more verbose

Good point. The whole idea of the proposal is to make the incomplete records mandatory so you always know if the call has an implicit config. Having std as a special case is too weird, but otherwise, it's too verbose :smiling_face_with_tear:

view this post on Zulip Luke Boswell (Mar 07 2024 at 09:11):

This has the added benefit that when you see a {} you know it has to be an empty/unit record

view this post on Zulip Norbert Hajagos (Mar 07 2024 at 11:26):

This has the added benefit that when you see a {} you know it has to be an empty/unit record

Holy ... I never realised this is how a call would look like when you use the default value for every field. I always assumed it was a unit. That seems cursed :laughing:

view this post on Zulip Richard Feldman (Mar 07 2024 at 12:35):

a downside of this idea is that it introduces a new type of breaking change.

Today if I add a ? field to a record that doesn't have any, that's a nonbreaking change because all the call sites will keep working. In this design, that would become a breaking change because all the call sites would need to add ..

view this post on Zulip Kiryl Dziamura (Mar 07 2024 at 13:00):

Yeah, there are only two options at first glance: breaking change with verbose std (which might be restructured by splitting functions with defaults into multiple specific functions. crazy, I know) vs loose records, so you never know if there are defaults until you check.

view this post on Zulip Kiryl Dziamura (Mar 07 2024 at 13:08):

Speaking of the new type of breaking change in particular- it's only if you haven't used .. already :D
On the other hand, you read code more often than you change it, right? Known unknowns vs unknown knowns concept fits as well

view this post on Zulip Artem Shamsutdinov (Mar 07 2024 at 15:10):

I am not comletely sure about this change, but I wanna say is that it looks like this syntax would discurage both users and lib authors to use defaults and would encouarage to specify everything explicitly, which can be good

I recongnize that there definitely are situations where defaults are necesseary. So this syntax will be an escape hatch when you don't care, but at list this way "not caring" is explicit instead of being acidental

Another way to consider Richard's point is that this "breaking" behaviour might be a positive thing:

If lib user is not using defaults anywhere (writes everything explicitly) and lib authors introduce a change that adds some new defaults - then user will see a compiler error that would force him to evaluate if this new default is good or not or he might want to tweek the stuff and pass it explicitly. Otherwise user wouldn't know the new parameter is now available for overrides

view this post on Zulip Brendan Hansknecht (Mar 07 2024 at 16:37):

At the same time, it is pretty common to add default args that are already the default. Like a user requests more flexibility for a function. So you add a new default arg. For all other users nothing changes. For that one user, they get the extra power they requested. Definitely don't want that to be a breaking change

view this post on Zulip Artem Shamsutdinov (Mar 07 2024 at 17:15):

Yeah, seems like "adding new default property" is always about "adding extra powers" and maybe extra powers are not worth to be breaking

view this post on Zulip Kiryl Dziamura (Mar 08 2024 at 22:05):

Sometimes extra powers can lead to bad design decisions. I agree that breaking defaults on boundaries can be an annoying problem. However, they can be silent problems internally, where you have a method of god and then just use different combinations of parameters.

Once I read through a codebase with multiple layers of functions with defaults. I had to jump back and forth trying to keep in mind the optional parameters that were quite impactful and not extra (an important note it was Python with default args, not records).

Also, I assume the breaking change on the boundaries will be avoided with the inversion of configs to function names f { a, b } -> f_a x f_b x`. Not sure if it’s good or bad. Rust works this way and it significantly impacts at least std interfaces.

It encourages granularity and readability but makes the learning curve more steep as it would mean more noisy functions in the library documentation.

Btw, speaking of the Default trait. It describes defaults that are applied to all instances of the struct, while defaults in records are defined at the destructuring site so they have no source of truth. It means that if you build a configuration record and use it for different functions, the result is not predictable, especially in the case of indirection and/or copypasted functionality that changes over time:

f = \{ x, y ? 1 } -> x + y
g = \{ x, y ? 2 } -> x + y

config = { x : 42 }

f config
# ...
g config

But, it’s simple and it should be enough most of the time.

view this post on Zulip Brendan Hansknecht (Mar 09 2024 at 00:02):

Yeah, I think many of the problems you mentioned are mitigated by making it all part of the type. Then you have a config type with all of the defaults stored with it. Sadly, I don't think that design works with roc.

view this post on Zulip Brendan Hansknecht (Mar 09 2024 at 00:03):

As part of the type, it tends to be less common that it is used with unique variants for every individual function. And when it is, you will see and explicit named type with the info


Last updated: Jun 16 2026 at 16:19 UTC