-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Structural Records #2584
RFC: Structural Records #2584
Conversation
Co-Authored-By: Centril <[email protected]>
This looks like a super well thought out and comprehensive RFC. It might be worth clarifying the behaviour of struct RectangleTidy {
dimensions: {
width: u64,
height: u64,
},
color: {
red: u8,
green: u8,
blue: u8,
},
} Presumably, there will be no "magic" here, and you will only be able to derive traits which are implemented for the anonymous structs themselves. Another question is around trait implementations: at the moment, 3rd party crates can provide automatic implementations of their traits for "all" tuples by using macro expansion to implement them for 0..N element tuples. With anonymous structs this will not be possible. Especially with the likely arrival of const generics in the not too distant future, negating the need for macros entirely, anonymous structs will become second class citizens. Is this a problem, and are there any possible solutions to allow implementing traits for all anonymous structs? |
other_stuff(color.1); | ||
... | ||
yet_more_stuff(color.2); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fn do_stuff_with((red, green, blue): (u8, u8, u8)) {
some_stuff(red);
other_stuff(green);
yet_more_stuff(blue);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even better, with #2522:
fn do_stuff_with((red: u8, green: u8, blue: u8)) {
some_stuff(red);
other_stuff(green);
yet_more_stuff(blue);
}
However, while if you write it in this way it is clear what each tuple component means, it is not clear at the call site...
let color = (255, 0, 0);
// I can guess that this is (red, green, blue) because that's
// the usual for "color" but it isn't clear in the general case.
blue: u8, | ||
}, | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This syntax looks very similar to another already accepted RFC, but semantics seems different - #2102.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True; I have to think about if some unification can be done in some way here.
Thanks!
This bit is not currently well specified, but it should be so I will fix that (EDIT: fixed)... I see two different ways to do it:
Yup. Usually up to 12, emulating the way the standard library does this.
Nit: to not have to use macros for implementing traits for tuples you need variadic generics, not const generics. :)
I think it might be possible technically; I've written down some thoughts about it in the RFC. However, the changes needed to make it possible might not be what folks want. However, not solving the issue might also be good; by not solving the issue you add a certain pressure to gradually move towards nominal typing once there is enough operations and structure that you want on the type. |
standard traits that are implemented for [tuples]. These traits are: `Clone`, | ||
`Copy`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Debug`, `Default`, and `Hash`. | ||
Each of these traits will only be implemented if all the field types of a struct | ||
implements the trait. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's... quite a bit of magic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree; this is noted in the drawbacks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideas for possible magic reduction discussed below in #2584 (comment).
+ For `PartialEq`, each field is compared with same field in `other: Self`. | ||
|
||
+ For `ParialOrd` and `Ord`, lexicographic ordering is used based on | ||
the name of the fields and not the order given because structural records |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With macros fields can have names with same textual representation, but different hygienic contexts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does that change anything wrt. the implementations provided tho? I don't see any problems with hygiene intuitively, but maybe I can given elaboration?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably relevant to other places where the order of fields needs to be "normalized" as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. :) But I'm not sure if you are pointing out a problem or just noting...
Any sorting / "normalization" is done post expansion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear to me how to sort hygienic contexts in stable order, especially in cross-crate scenarios. That probably can be figured out somehow though.
```rust | ||
ty ::= ... | ty_srec ; | ||
|
||
ty_srec ::= "{" (ty_field ",")+ (ty_field ","?)? "}" ; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think types can start with {
syntactically.
There are multiple already existing ambiguities where parser assumes that types cannot start with {
, and upcoming const generics introduce one more big ambiguity ({}
is used to disambiguate in favor of const generic arguments).
#2102 uses struct { field: Type, ... }
to solve this (and also to discern between struct { ... }
and union { ... }
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about expressions/patterns.
Unlimited lookahead may also be required, need to check more carefully.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are multiple already existing ambiguities where parser assumes that types cannot start with
{
Such as? (examples please...)
Const generics allow literals and variables as expressions but anything else needs to be in { ... }
.
This is not ambiguous with structural records (because the one-field-record requires a comma...) but requires lookahead.
Unlimited lookahead may also be required, need to check more carefully.
Scroll down ;) It's discussed in the sub-section "Backtracking".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's discussed in the sub-section "Backtracking".
It's a huge drawback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Such as? (examples please...)
Nothing that can't be solved by infinite lookahead, but here's "how to determine where where
clause ends":
fn f() where PREDICATE1, PREDICATE2, { a: ...
Does { a: ...
belong to the function body or to the third predicate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@laaas Because it is inconsistent with Tuples/Tuple-structs and also less ergonomic.
I don't think it's confusing for readers, the one-field case is clearly disambiguated by an extra comma and that is consistent with how tuples are treated. In other words, if { x, }
is confusing, then so is (x,)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's going to be drawbacks of this pattern for the structural records, either for slowing parser or cause types can't start with {
syntactically, couldn't we use .{
instead?
Inspiration by Zig programming language
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I've been thinking about this recently and I've come to the conclusion that if we lean into the ambiguity, then this is an non-issue.
The need for backtracking here stems from ambiguity with type ascription. This can be fixed by simply assuming that { ident:
is always going to be a structural record, either in expression or type position (also solves the const generics issue). That is, if given { foo: Vec<u8> }
in an expression context, we will attempt parsing this as a structural record, and then fail at <
with a parse error (backtracking can be done inside diagnostics only if we want to). This way, we have retained LL(k)
(standing at {
, the :
is 2 tokens away and so therefore k = 2
in this instance).
If a user wants to disambiguate in favor of ascription, they can write { (ident: Type) }
. Disambiguating in this way should not be a problem as { ident: Type }
is not a very useful thing to write. Specifically, in the case of PREDICATE2, { a: ...
, this should not be a problem because the return type itself is a coercion site and informs type inference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would it fail at <
? If you're trying to parse Vec<u8>
as an expression, wouldn't the <
be parsed as less-than, and you only fail when you see >
(and in general arbitrarily far away)?
It seems like the same issue as when we tried to get rid of turbofish...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@comex Oh; yeah, you're right. It should fail at >
. Still, we can take a snapshot before trying to parse the expression after ident:
and backtrack if it turned out to be a type and provide good MachineApplicable
diagnostics here.
It seems like the same issue as when we tried to get rid of turbofish...
Well in the sense that turbofish would also be unnecessary if we are OK with backtracking, but it seems like some people object to backtracking, so I've come up with a solution using LL(k)
at the cost of not being able to recognize { binding: Type }
as a block with a tail-expression ascribing binding
. Because such code is, in my view, pathological, that should be fine. It's much less of an issue than turbofish from a learnability POV imo.
field_init ::= ident | field ":" expr ; | ||
``` | ||
|
||
Note that this grammar permits `{ 0: x, 1: y }`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#1595 was closed, so I'd expect this to be rejected.
This saves us from stuff like "checking that there are no gaps in the positional fields" as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah probably; I've left an unresolved question about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
struct A(u8);
let _ = A { 0: 0u32 };
is accepted, so this is currently inconsistent as it stands anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@varkor yeah that was accepted to make macro writing easier (and it totally makes it easier...!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@petrochenkov I am not sure I understand the reasoning why this is not allowed:
struct Foo {
0: i32, // This is the only thing that fails ...
arg_1: i32,
named_1: i32,
}
fn main() {
let foo = Foo { 0: 0, arg_1: 1, named_1: 42} ;
println!("{}", foo.0);
}
fails only at the struct definition with "^ expected identifier". Would it not just be necessary to change 'IDENTIFIER' to 'IDENTIFIER_OR_NUMBER' (pseudo-code wise)?
to make it all work as everything else seems to be able to work with '.0', '0: ', etc.?
I am also not seeing the drawback given that it is already inconsistent ...
My personal motivation with that is to mirror named function arguments in structs as they then perfectly match the:
0: arg_0, 1: arg_1, named_1: arg_3
pattern. Also getting that (0: as allowed identifier in structural records) in independent of structural records is one less thing that structural records need to handle.
To be precise: I am only suggesting to allow numbers as identifiers, nothing else - so there is still plenty of distinction from Tuple to Struct, but they can be 1:1 mapped, but as for every other identifier you need to be explicit about the 0, 1, ...:
Tuple (i32, i32) ~= Struct { 0: i32, 1: i32 }
I think the language should be frozen for large additions like this in general, for a couple of years at least. |
The trait auto-implementation magic worries me, mostly from the point of view of things not the compiler wanting to implement traits on things. Today crates can do a macro-based implementation for the important tuples, and I can easily see a path to variadic generics where the impls could easily support any tuple. But I can imagine I'd also be tempted to block this on changing struct literal syntax to using Overall, I think I feel that this is cool, but not necessary. |
I'm very concerned with properties of the struct explicitly relying on lexicographic ordering. I don't think that I think that defining lexicographic behaviour for But as a user, I would be surprised to find the below code fail: let first = { foo: 1, bar: 2 };
let second = { foo: 2, bar: 1};
assert!(first < second); |
@clarcharr I've added an unresolved question to be resolved prior to merging (if we do that...) about whether
I would have loved if we had used
Sure; I entirely agree with that sentiment; it is by far the biggest drawback.
I noted somewhere in the RFC that with the combination of const generics and variadic generics, you may be able to extend this to structural records as well. For example (please note that this is 100% a sketch), using a temporary syntax due to Yato: impl
<*(T: serde::Serialize, const F: meta::Field)>
// quantify a list of pairs with:
// - a) type variables T all bound by `serde::Serialize`,
// - b) a const generic variables F standing in for the field of type meta::Field
// where `meta` is the crate we reserved and `Field` is a compile time
// reflection / polymorphism mechanism for fields.
serde::Serialize
for
{ *(F: T) }
// The structural record type; (Yes, you need to extend the type grammar *somehow*..)
{
// logic...
} This idea is inspired by Glasgow Haskell's |
the amount of work, and problem this RFC come with is not worth the price. |
For anyone that would still like to use Structural Records, as of this writing there are two crates to try:
Full Disclosure: I haven't tried either. Just suggestions for anyone that ends up here. |
Use case: Named arguments for mut drawfn: impl FnMut(&mut Vec<Vertex>, f32, f32, &[u8], usize, Color) This would be clearer as mut drawfn: impl FnMut({
vertex_buffer: &mut Vec<Vertex>,
x: f32,
y: f32,
data: &[u8],
idx: usize,
color: Color
}) Rust doesn't have direct support for named arguments in |
You would still need to look doc to see name.
not important
?
I really advice to think about it struct Draw<'a, 'b> {
vertex_buffer: &'a mut Vec<Vertex>,
x: f32,
y: f32,
data: &'b [u8],
idx: usize,
color: Color,
} This may seem verbose but that force you to think twice about your api, if this struct look ugly for you this probably mean you could use some structure to better group logical variable (maybe a tuple for |
Are you taking about the argument names?
That's your opinion. In my opinion ergonomics are important.
I think my arguments are just fine, thank you. I just want names for them, like you can have for regular functions. |
Oh sorry I miss the fact your case is about FnMut Trait. Generally if you use Fn family trait I would advice to keep it very simple, they are mostly for closure that should have 1 or 2 argument max. Thus I maintain my point but I will add that you may use a trait like: trait Draw {
fn draw(
&mut self,
vertex_buffer: &mut Vec<Vertex>,
x: f32,
y: f32,
data: &[u8],
idx: usize,
color: Color,
);
}
// this allow auto closure impl and function
impl<T> Draw for T
where
T: FnMut(&mut Vec<Vertex>, f32, f32, &[u8], usize, Color),
{
fn draw(
&mut self,
vertex_buffer: &mut Vec<Vertex>,
x: f32,
y: f32,
data: &[u8],
idx: usize,
color: Color,
) {
self(vertex_buffer, x, y, data, idx, color)
}
} This would result in |
Why this arbitrary restriction? Should regular functions also have 1 or 2 arguments max? If not, what makes closures so different?
I have no idea what I'm looking at honestly. |
I think yes, function should have max 2, and on few case 3 arguments. If you look at std I think there are few 3 arguments function and probably very few that have more than 3 (exclude self). If a function require a lot of argument it's become difficult to remember argument order and you can easily make mistake, cause, no named argument. (That also why I think function should not have argument of same type for +3 argument count). That also why I disadvise tuple in public api that have +3 elements. While named argument is a feature very close to structural record we both lack of them in Rust. I argue that it's not a bad thing cause it's force dev to make greater api. With these features a dev could easily choice the easy way to have function with a lot of argument. Most python lib I used are like that, and honestly I don't like it at all, I already used function that have 10 arguments, it's not something I would like in Rust crate. Rust is build around structural programing, define struct, define trait is rusty. I think Rust lang is doing dev a favor to force them to be verbose, to force them to use structure or trait that structure the code, force you to have strong typing. I think having naming function argument or naming structure record would make more evil than good. If I would be a user and I see this: mut drawfn: impl FnMut({
vertex_buffer: &mut Vec<Vertex>,
x: f32,
y: f32,
data: &[u8],
idx: usize,
color: Color
}) I would clearly not like it, there is too many argument, I would probably need to make a function cause a closure would be too big for that, also there would be no direct documentation. My trait solution would solve most of these problems and would allow you a very nice documentation (bonus rust-analyzer would make it very fast to impl it) but I think you could also refine your api, surely data and idx could be linked, and vertex_buffer x y look also linked. /// write something useful
struct VertexBuf<'a> {
buf: &'a mut Vec<Vertex>,
x: f32,
y: f32,
}
/// write something useful
struct Data<'a> {
data: &'a [u8],
i: usize,
}
trait Draw {
/// write something useful
fn draw(&mut self, vertex_buffer: VertexBuf, data: Data, color: Color);
}
fn some_function(mut drawfn: impl FnMut(VertexBuf, Data, Color)) {}
// or using trait
fn some_function(mut draw: impl Draw) {}
I think we are on topic, sorry if I started an off topic conversation. |
I agree with many arguments in a function is a very bad practice. But forcing user to declare a struct that might be used only in one function is also not an ideal solution.
Sometimes arguments are added to a function one-by-one as part of some small commit here and there. In this case, especially on a bigger project, it is very rare what code will be refactored (due to time constraints or other reasons) just to make API look correct. It is probably better to allow at least some way of naming argument in-place to increase overall code readability. Maybe I'm the unlucky one here but on most projects with 30+ people I was on this is the reality. IMHO |
Counter example: rustc codebase
The standard library APIs have a fairly simple job of exposing OS APIs and some core data structures. The rustc developers have better things to do than try to play type tetris to cram everything into structs so everything meets the arbitrary requirement of at most 3 arguments per function. |
@crumblingstatue I don't see how that prove anything or change anything to my point, never I said the quality of rustc implementation was great. Your example only reinforce my opinion that such function is a nightmare to maintain. Also, these functions are mostly call once and are totally different that the use case you bring up and about public API design I try to explain since 3 messages. I have the feeling you don't understand my points. I will so stop arguing more about this further since this go nowhere with the last two messages. |
This is an unusual reason to close an RFC in an open source project. "This is a feature that we should have" warrants a formally positive response, even if it is too costly for a particular set of contributors to justify implementing at this time. My instinct would be to keep the RFC open, but allocate no resources to it. If a perfect implementation falls in our lap, we'd be happy to accept it. Just don't take time away from other work to implement it. Rust is open source. If prior non-contributors really want to see this feature happen, they might decide to work on it themselves, despite never previously having interest to work on rust. After working on it, they might become regular contributors. But without leaving this window open, they might never join the project. There is a feedback mechanism between the resources available to a project and the contributions the project is willing to accept. Clearly communicating an openness to improvements, even if most of the current contributors think the improvements are not worth their effort, could increase the amount of resources available to a project, compensating for any incurred costs. |
At first I agreed with this point, usually such RFC's are closed as postponed rather than closed. But on reading the history of this issue, I think it is an appropriate choice. The point is that this is not a change that can be accomplished by an RFC, this needs something closer to a lang team initiative or a working group to get done, because it has far reaching effects on all other parts of the language. And there is not enough bandwidth on the team to consider that in the foreseeable future, and by the time it comes around a new "v2" RFC will be needed anyway, even though it may draw heavily on aspects of this one. A contributor dropping an implementation on the team's lap wouldn't solve all the other issues around interaction with other features (present and future) which are part of the current vision for the language. I think the best way to a motivated contributor to push this forward would be to drum up support for a working group about it. |
To extend @digama0's point here, I don't think this is a case where, necessarily,
Adding a new fundamental kind of type is a pervasive impact. It's not just a lang question, it's now got type-team impact, opsem-team impact, miri impact, libs-api impact, proc macro impact, different-backends impact, etc. I pretty much feel the same now as I did two years ago (#2584 (comment)): the way forward is not to try to make the whole new type kind right away, but to look at the motivation and attempt to address some of those pieces in a lower-impact way. What parts of the goodness of this can be done in a way that they can mostly reuse existing inference, existing MIR, existing What makes it hard to define a type? What makes it hard to use a type? Can we introduce shorthands for common cases? Could we allow them in more places? Could this mostly be done with Voldemort types, rather than structural ones? Etc. (Just speaking for me, not stating a lang team position here.) |
TLDR;The syntax of strutural and nominal record access should differ so that most changes occur only at the compiler frontend. Longer versionAs far as I know, structural records and tuples carry the same semantics, so why can't we implement structural record as a syntactic sugar of tuples? Doing so requires minimal changes to the backend, i.e. type checking, Miri, code generation etc. For this to work, the syntax of structural records access has to be different from that of nominal records (the current struct system), so we don't have to incorporate extra mechanisms into the type-checker to disambiguate structural record access from nominal record access, which is probably the main reason why some think this proposal is pervasive. For the sake of explanation, let's pretend the syntax of structural record access looks like this:
For example:
Other syntactical aspects of structural records, such as type annotation, construction, and destructuring should already be distinguishable from nominal records, thus they require no syntax changes. As a proof of concept:
In conclusion, as long as we keep structural and nominal record access syntax distinct, no pervasive changes are needed. |
If let a = if cond() {
let b: {x: usize, y: usize} = (0, 1);
b
} else {
let c: {y: usize, z: usize} = (2, 3);
c
};
a#y Does the last line desugar to I don't think the |
Sorry, I miscommunicated earlier. For Step 2.1, the desugars only happen after type-checking is completed without error. In other words, the type checker is aware of the existence of structural record type.
This question is invalid because the type of So again, most of the changes are on frontend, namely parsing and type checking (or type inference). |
If it's a separate type, then this isn't a desugaring at all, as it has to participate in the whole frontend and middle end. The MIR uses the same type system as the type checker (except for erasing regions after borrowck). At that point Another issue that arises: Is
|
C# went down this road. It works reasonably well, but it's still risky. For example, this compiles https://dotnetfiddle.net/fYHBJa public static (int B, int A) Foo() => (1, 2);
public static void Main()
{
(int A, int B) pair = Foo();
Console.WriteLine(pair);
} and doesn't do what you'd want from a proper structural type. So you can sortof do it, but to make it work well you have to add a ton of random little "oh, wait, we'd better lint that" stuff. Not to mention all the "and what do you get from Thus once you want it to be preserved through closures, say, all the "kinda like a type but isn't" are maybe even worse than having it be a real type in the first place. (This reminds me of all the |
edit: please discuss on internals since here is waay too cluttered original post: // in std::somewhere
pub struct AnonymousStruct<Fields: std::marker::Tuple, const NAMES: &'static [&'static str]>(pub Fields);
impl<Fields: std::marker::Tuple, const NAMES: &'static [&'static str]> AnonymousStruct<Fields, NAMES> {
/// Self::get_field_index("abc") = Ok(5) means self.abc desugars to self.0.5
pub const fn get_field_index(name: &str) -> Result<usize, ()> {
const { assert!(NAMES.is_sorted(), "NAMES must be sorted"); };
NAMES.binary_search(&name).map_err(|_| ())
}
} pseudo-code: fn desugar_anonymous_struct_type(fields: Vec<(Member, Type)>) -> Result<Type> {
// sorts by field name
let mut fields: BTreeMap<Member, Type> = fields.into_iter().collect();
let mut tuple_fields: Punctuated<Type, _> = (0..).map_while(|i| fields.remove(&Member::Unnamed(i.into()))).collect();
if fields.is_empty() {
// actually a tuple, since all fields are named using successive integers
return Ok(TupleType { paren: Paren::default(), elems: tuple_fields }.into());
}
if !tuple_fields.is_empty() {
bail!("all fields must be named");
}
let mut keys = vec![];
let mut types = vec![];
for (k, v) in fields {
let Member::Named(k) = k else {
bail!("all fields must be named");
};
keys.push(LitStr::new(&k.to_string(), k.span()));
types.push(v);
}
Ok(parse_quote!(::std::somewhere::AnonymousStruct<(#(#types,)*), { &[#(#keys),*] }>))
} I would expect the code for the field access operator |
desugaring to a named struct in |
created an internals thread for discussing my desugaring idea: https://internals.rust-lang.org/t/structural-types-again-desugar-to-named-struct-in-std/18550?u=programmerjake please discuss on internals since here is waay too cluttered |
This sounds so right, guys, please implement it... |
🖼️ Rendered
📝 Summary
Introduce structural records of the form
{ foo: 1u8, bar: true }
of type{ foo: u8, bar: bool }
into the language. Another way to understand these sorts of objects is to think of them as "tuples with named fields", "unnamed structs", or "anonymous structs".💖 Thanks
To @kennytm, @alexreg, @Nemo157, and @tinaun for reviewing the draft version of this RFC.
To @varkor and @pnkfelix for good and helpful discussions.
Closure comment