Record types

Is there a reason why Rust doesn't support:

type R = {x: f64, y: f64};

I believe it's possible if these structural record types are differentiated by field order. For example, in the following program, R1 and R2 are different types:

type R1 = {x: f64, y: f64};
type R2 = {y: f64, x: f64};

They contain the same fields, but in different order. Then... we could probably allow implicit conversions between them. I believe implicit conversions can be inefficient if there are dozens of fields ― but that's not the case. Any opinions on that?

An use-case for that is when you want anonymous record types. For example:

struct C {
    m_x: {x: f64, y: f64},
}

Currently, you can only have nominal record types in Rust. Either struct R {} or enum variant (using enum E { R {...} }).

Anonymous Initialization

How would one initialize an object of an anonymous recorrd type? Something like TC39 Record & Tuple proposal should do.

let r: {x: f64, y: f64} = #{x: 0.0, y: 0.0};

Or _{x: 0.0, y: 0.0}.

I think you lose coherence in the type system. This property states that every different valid typing derivation of a program leads to a resulting program that has the same runtime semantics.

Anonymous record types seems to lead to situations where records are nominally generic, it's not always clear when you're constructing nominally equal types? Should an anonymous struct from another library have some sort of structural type assigned or should it be secretly nominally typed and then fail to unify elsewhere.

Either way gets confusing, I think.

Maybe it's worth exploring! I'm not sure myself as I'm no type system expert!

Anonymous record types should have no connection to each other. However there would be a type inference involved when you initialize an anonymous record type... Just that. And implicit conversions when you assign between compatible anonymous record types ("compatible" is when types have same fields, but different order).

Sounds like you could give this a go with macros. Just expand it into a nominal type with some mangled name. Add some reflection capabilities to convert between the hidden structs. I don't think you can make the conversion implicit, but you can get a feel for how useful it is anyway?

1 Like

Can macros be used as type expressions? I'm already having some trouble on the playground with:

macro_rules! T {
    () => f64;
}

Diagnostic:

   Compiling playground v0.0.1 (/playground)
error: macro rhs must be delimited
 --> src/main.rs:2:11
  |
2 |     () => f64;
  |           ^^^

And I'll also want to initialize the anonymous record anyway.

There was an RFC about them; here's the comment where lang decided not to have them for at least the "foreseeable future":

And here's my personal take on things:

2 Likes

The very early Rust had structural objects in 2010.

1 Like

Yes, macros can be used in type position.

macro_rules! T {
    () => { f64 };
}

let _: T![] = 0.0;

The delimiters around the macro's output (or input) TokenStream is just to delaminate the output, and isn't actually relevant to what the macro expands to. For macros which want to expand to an actual expression block, they need to include that expression block inside the delimiters; you'll often see => {{ /* output */ }}.

2 Likes

This could also let RFC 1506 extend to unnamed tuples, so you could match (A, B, C, D, E) with a pattern like { 2: c, .. }.

1 Like

It's not incoherent. Think of it as tuples where the fields have names. Records with the same fields are considered the same type, just like tuples with the same fields are considered the same type.

This is actually a big strength of this proposal, because it allows different libraries to use the same types, without having to depend on a common crate that exports these types. For example, two independent crates that both use a record type { red: u8, green: u8, blue: u8 } could interoperate seamlessly.

A drawback of records is that you can't add methods to them or implement traits from another crate because of the orphan rules. This makes them significantly less useful.

I guess a big motivation for records is to simulate named arguments:

foo({ bar: 5, baz: true })

I think this is an important use case, but I'd rather have named arguments supported directly by the language, i.e.

foo(bar: 5, baz: true)
2 Likes

Note that there's no need for structural records for that. It would work with existing nominal records with two things:

  • A way to create a struct literal of inferred type, perhaps foo(.{ bar: 5, baz: true })
  • Sugar to create an anonymous type in a function definition, like fn foo(.{ bar: i32, baz: bool})

Then you don't have to change the type system at all, which makes the whole thing massively easier to deal with.

3 Likes

In my experience with graphics programming, in situations where you want that set of fields and have interoperability considerations, you also often want #[repr(C)] #[derive(bytemuck::Pod)] or similar — guaranteeing the representation, so that you can interconvert between &[Color] and &[u8] (the pixels of an image), or between Color and [u8; 3] or another vector representation (for more generic mathematical or bytewise operations).

So, a default-repr struct with no non-std trait impls — which is presumably what a structurally-typed record would be — wouldn't serve these use cases. (Of course there are other cases where it is simply passed by value to/from functions, where it would do just fine.)

1 Like

NOT A CONTRIBUTION

Whatever happened to the _ { bar: 5, baz: true } syntax? Has consensus been reached on that being undesirable, or is this just your preferred syntax?

It's just an arbitrary preference for the time being, as far as I'm aware. _ for a type name to me "feels" appropriate in pattern position, but potentially a bit more questionable in expression position; there's also other (extremely experimental) potential feature proposals which use _ in expression position (e.g. autoclosure).

1 Like

Where's the proposal for the .{} or _{} syntax?

That's just the one I'd used in that old comment to which I'd linked. I don't think there's any consensus on the desirability of the feature nor its syntax.

I've been trying out the dot syntax, inspired by Swift, since _:: is pretty noisy. Worse, the way we actually spell "get a thing from an inferred type in an expression" today is the even longer <_>::, but I think "you can use <_>::SeqCst" would just turn into another "lol rust's terrible syntax" meme. And I don't think that std::_::min(a, b) should ever work, so using something that looks less like a path might be good anyway.

What'll actually happen? No idea. But I like having a couple different variations in the discussion anyway.

3 Likes

I don't think an RFC for this exists yet, it's just a simple idea: A struct literal, e.g. Struct { field: value } can be written as _ { field: value }, when the type can be inferred, e.g. in a function argument. This is more concise and reduces boilerplate by requiring fewer imports. The _ as a placeholder feels like the obvious choice, since it's already used for omitting a type that can be inferred, e.g. Vec<_>.

The syntax { field: value } without an underscore is also a possibility, and looks appealing because JavaScript and Python have a similar syntax. However, it has some problems (which also apply to record types as proposed in this thread btw):

First, it's ambiguous with blocks in several situations, e.g.

  • when there are no fields: {}
  • when there's a single field using the shorthand syntax: { field } instead of { field: field }
  • when all fields are omitted: { ..default() }

JavaScript's syntax has a deliberate limitation because of this: When returning an object from an arrow function, the object must be wrapped in parentheses, e.g. (x, y) => ({ x, y }). But note that adding parentheses would not work in Rust, since blocks are expressions in Rust, and can appear anywhere.

The other problem with the { field: value } syntax is that it looks so similar to a block that it may negatively impact diagnostics. For example, when you write Struct { field = 42 }, Rust will tell you to replace the = with a :. This useful diagnostic likely wouldn't work for { field = 42 }.

What's nice about _{} or .{} is that it can be trivially extended to other parts of Rust. For example, _::Variant could become a shorthand for SomeEnum::Variant. Tuple structs and unit structs could be inferred as well with _() and _, although _() would be more controversial, because constructing a tuple struct is just a function call, strictly speaking.

One common argument against any proposal to make syntax more concise is explicitness: People argue that being forced to specify types explicitly makes the code easier to understand. I'd say this ship has sailed, since Rust already has powerful type inference and doesn't require specifying types in many places. Also, Rust's design has always been about giving programmers as much freedom as possible, instead of forcing them to follow certain paradigms. Rust devs can specify types everywhere if they think it helps them, but they don't have to.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.