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 }aespecially because I'd feel like writing it as
{ name : Str, email : Str } a -> { name : Str, email : Str } awhich 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.
IMO the syntax should be something like {name: Str} & a - with an explicit separator, and no specific requirements around whitespace (or lack thereof)
Or maybe {a & name: Str}, to align better with the current update syntax
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 }* -> ...
the open tag union syntax doesn't come up as often anymore, but open records get inferred a lot
e.g. if I write \{ x, y } -> x + y in the repl, the inferred type is an open record
and I liked the idea of having * be something you could tack on the end to make a record open
7 messages were moved here from #beginners > My reaction to Roc, as a beginner by Richard Feldman.
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:
similar to how tag unions now work
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
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)
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
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?
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.
yeah my immediate thought is that if you want to distinguish between the two, you want an opaque type
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
I can't remember running into any bugs around this in my JS days :thinking:
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.
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.
Very true. Even when there are overlapping types, there's a decent chance of subtle spelling/naming differences, such as nameFirst vs firstName, etc.
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
they'd be equally efficient at runtime; everything monomorphizes to closed anyway :big_smile:
Last updated: Jun 16 2026 at 16:19 UTC