Stream: ideas

Topic: open record syntax


view this post on Zulip Joshua Warner (Mar 24 2023 at 21:23):

Leonardo Taglialegne said:

The first weird thing I found is the syntax for open records which is just... Weird
{ name : Str, email : Str }a -> { name : Str, email : Str }a especially because I'd feel like writing it as
{ name : Str, email : Str } a -> { name : Str, email : Str } a which feels... even weirder :thinking:

FWIW I was working on rewriting this part of the parser a couple days ago, and I agree it's weird. With the added whitespace, that can accidentally mean something completely different. e.g.:

Foo {name: Str}a Bar # a type with two type arguments
Foo {name: Str} a Bar # a type with three type arguments

Most likely you're going to get a type error later, but that's pretty confusing for a user.

view this post on Zulip Joshua Warner (Mar 24 2023 at 21:32):

IMO the syntax should be something like {name: Str} & a - with an explicit separator, and no specific requirements around whitespace (or lack thereof)

view this post on Zulip Joshua Warner (Mar 24 2023 at 21:33):

Or maybe {a & name: Str}, to align better with the current update syntax

view this post on Zulip Richard Feldman (Mar 24 2023 at 22:00):

so part of the original motivation there was to make open record and open tag union syntax more concise with * specifically, e.g.

foo : { x : F32, y : F32 }* -> ...

view this post on Zulip Richard Feldman (Mar 24 2023 at 22:01):

the open tag union syntax doesn't come up as often anymore, but open records get inferred a lot

view this post on Zulip Richard Feldman (Mar 24 2023 at 22:01):

e.g. if I write \{ x, y } -> x + y in the repl, the inferred type is an open record

view this post on Zulip Richard Feldman (Mar 24 2023 at 22:02):

and I liked the idea of having * be something you could tack on the end to make a record open

view this post on Zulip Notification Bot (Mar 24 2023 at 22:02):

7 messages were moved here from #beginners > My reaction to Roc, as a beginner by Richard Feldman.

view this post on Zulip Richard Feldman (Mar 24 2023 at 22:03):

that said, something I don't think we've ever discussed is the idea of records being open by default in the argument position :thinking:

view this post on Zulip Richard Feldman (Mar 24 2023 at 22:03):

similar to how tag unions now work

view this post on Zulip Richard Feldman (Mar 24 2023 at 22:03):

in other words, if you write foo : { x : F32, y : F32 } -> ... you can just pass it a record with more fields than x and y, and it will Just Work

view this post on Zulip Richard Feldman (Mar 24 2023 at 22:04):

I can see pros and cons to that idea (and @Ayaz Hafiz might also have a comment as to whether that's even feasible, or might have unexpected pitfalls)

view this post on Zulip Richard Feldman (Mar 24 2023 at 22:10):

one thing I like about the idea is that learners would likely encounter the concept of type variables in records later on, which is nice for learning curve

view this post on Zulip Luke Boswell (Mar 25 2023 at 00:41):

I like this idea. I imagine it would be essentially syntax sugar for creating a new record with just those fields in it? Is there a particular use case where you definitely need a closed record?

view this post on Zulip Ayaz Hafiz (Mar 25 2023 at 00:44):

I have thought about this a bit and I thought I had a larger message regarding some particulars, but I can't seem to find it now. Apologies.

If I remember correctly, though, my thoughts boil down to the following two points:

Vector3 : { x : U64, y : U64, z : U64 }

norm2 : { x : U64, y : U64 } -> U64

Now, it is true that you can take the 2-norm of a 3-dimensional vector - however, it may be unlikely that you wanted to do so. With closed types in argument position, you can catch potential bugs like this; if you don't have a way to opt-out, you can't.
More generally, I believe there are cases where having a "larger type" is semantically different from saying a type is a superset of another (i.e. you probably do in fact want to say Vector3 is distinct from Vector2, even though the former is superset of the latter).

Of course, another way to deal with this is with opaque types.

view this post on Zulip Richard Feldman (Mar 25 2023 at 00:45):

yeah my immediate thought is that if you want to distinguish between the two, you want an opaque type

view this post on Zulip Richard Feldman (Mar 25 2023 at 00:47):

Clojure always has the equivalent of open records, and I know Rich Hickey thinks this is really valuable - he talks about it in one of his talks, but I forget which one

view this post on Zulip Richard Feldman (Mar 25 2023 at 00:48):

I can't remember running into any bugs around this in my JS days :thinking:

view this post on Zulip Kevin Gillette (Mar 25 2023 at 03:18):

Is needing that degree of safety more common than the cases where we'll benefit from the convenience?

Depending on what the most common use-cases for Roc end up being, I could see it go either way. If it's mostly used for graphics programming, then we'd probably want closed-by-default. If many of the cases look like { name: Str, age: Int * }, then we'll probably want open by default.

view this post on Zulip Brendan Hansknecht (Mar 25 2023 at 03:36):

Even with cases like { name: Str, age: Int * }, how often do you really have lots of different types where you want to operate on the same subset of data. Generally, i assume you would generally choose a specific type like UserData, not the more general name and age.

view this post on Zulip Kevin Gillette (Mar 25 2023 at 05:15):

Very true. Even when there are overlapping types, there's a decent chance of subtle spelling/naming differences, such as nameFirst vs firstName, etc.

view this post on Zulip Leonardo Taglialegne (Mar 25 2023 at 11:45):

I think the { a & fields } syntax, being coherent with the record update one, would be a good choice. In particular, I would strongly downvote a whitespace sensitive option.

With regards to open-by-default, I would expect that real word use cases for open record only come up after one has passed the part of the learning curve where they learn about open records.

Plus, it is a good opportunity for a Helpful Error Message! If a user calls a function { x : F32 } with a value of type { x : F32, y : F32 } we can tell them

Hey, the types don't match. Maybe you wanted to define the function like this: { a & x : F32 } to allow extra fields?

The nuance is that we should only do that if the function is defined in the user code and not in packages

With regards to the type system... not opening by default would possibly allow for more efficient code? { x : F32 } can be represented by a float, { a & x : F32 } cannot

view this post on Zulip Richard Feldman (Mar 25 2023 at 11:50):

they'd be equally efficient at runtime; everything monomorphizes to closed anyway :big_smile:


Last updated: Jun 16 2026 at 16:19 UTC