fields-in-traits-rfc
fields-in-traits-rfc copied to clipboard
syntax for declaring fields
The existing RFC text proposes a struct-like syntax:
trait Foo {
f: T, g: U
}
This is nice in some respects, but awkward in others. For example, a ;
is required to separate fields from methods. There are a few alternatives floating about.
requirements
I think it's important to plan out how we will support shared-only fields (#5)
current proposal
trait Foo {
f: T, mut g: U;
fn foo();
}
impl Foo for Bar {
f: self.x.y,
g: self.x.z;
fn foo() { ... }
}
Upsides:
- consistent with a struct declaration -- except that
mut
keyword is not
Downsides:
- comma vs semicolon is awkward
-
mut g: U
is inconsistent with struct declaration
the let
keyword (or other contextual keyword)
One alternative is to use let
:
trait Foo {
// Shared-only field:
let f: T;
// Full mutable field:
let mut g: U;
}
impl Foo for Bar {
let f = self.x.y;
let g = self.x.z;
}
This no longer mirrors struct syntax, but it has some advantages:
- Permits
let mut f: T
to indicate (potentially) mutable fields. - Consistent with other trait items.
Downside:
-
let
suggests (to some, at least) "lexically scoped variables"
Alternative:
trait Foo {
field f: T;
field mut g: T;
}
inner struct
trait Foo {
struct {
f: T,
mut g: T
} // <-- no semicolon, I guess?
...
}
impl Foo for Bar {
struct {
f: self.x.y,
g: self.x.z
}
....
}
Advantages:
- clearly separates out the "data" part
- maybe provides a way for more flexible disjointness requirements
Disadvantages:
- can the fields be specified independently? syntax sort of suggests they are a group
- the
mut
part is not consistent with structs
Other interactions
- I've been considering a possible sugar where you can instantiate a trait if the only thing that is undefined (i.e., has no defaults) are field declarations. In that case, you could use a struct-like syntax. So, for example,
Foo { f: 22, g: 44 }
might instantiate the trait above. This would be equivalent to declaring, basically, a local struct with those fields. This can still work withlet
syntax, but does it feel weirder in some way? I guess not.
I'd think a mut
prefix works fine without the let
, like in function arguments. Another proposed was trait Foo { struct { f: T, g: U } ... }
, but maybe nobody likes that. It'd be good if the syntax left room for adding enum
variants to trait
s, but I suppose trait Foo { enum { f: T, g: U } ... }
works no mater the route chosen for fields, so no problem there.
You could put mut f: u32
, that's true. It still has the problems around semicolons. The embedded struct feels like a possibility. It seems sort of odd though to include mut
in those cases, since it looks so much like a struct declaration, but behaves differently (i.e., a regular struct doesn't permit mut
keywords in that spot).
I also like that let mut f: u32
makes each field an "independent item". This makes sense to me since I expect that in all other respects they would act like other items in a trait. The struct (to me) suggests that the fields are specified "as a group".
This could however be a benefit -- i.e., multiple struct
declarations might be used as a way to declare disjointness requirements in the future.
I have a pretty negative reaction to the let
syntax, because it goes afield of both the meaning of the word "let", and any existing use of it in Rust. Traditionally, "let" (in mathematics and elsewhere) is used to bind a variable to a value. The existing let
syntax in Rust does allow you to leave off the binding, but only when that binding is later specified (before it is used). Here, we use the same syntax as that latter, but with no clear matching binding.
We've also talked from time to time about allowing method declarations within struct declarations, as a shorthand for writing inherent impl blocks. Presumably we'd run into all the same problems there, but not have let
syntax as a way out?
I'm a bit surprised that ;
is required as a separator, rather than ,
. Can you elaborate on that?
Here, we use the same syntax as that latter, but with no clear matching binding.
I think the idea is that the "matching binding" is the assignment in the impl
? I.e.:
trait Foo {
let f: T;
let g: U;
}
impl Foo for Bar {
let f = self.x;
let g = self.y;
}
It is "specified before it is used" in the sense that if there is no matching impl
, it doesn't compile.
(FWIW I'm personally torn between the let
syntax and the struct { }
syntax, and would probably be fine with either one.)
You could use ref
and ref mut
since this represents some sort of indirection, but.. We should not use syntax that might confuse people just learning the borrowing dance, so that's probably out.
I fine let
kinda visually jarring for unclear reasons. Also, I agree Rust needs both ,
and ;
in blocks elsewhere anyways. I'm fine with let
semantically though because trait fields are some sort of aliased binding. And let
beats ref
on semantics. I'd assume adding alias
as a keyword is too big a breaking change.
I like the let
syntax, but agree that using let
in this context is weird.
I think using a contextual keyword like field
or member
would be best. It aligns well with type
IMHO.
trait Foo {
type Tf1: Bar;
type Tf2: Baz;
field f1: Tf1;
field f2: Tf2;
}
We've also talked from time to time about allowing method declarations within struct declarations, as a shorthand for writing inherent impl blocks. Presumably we'd run into all the same problems there, but not have let syntax as a way out?
True. Though last time I floated this, there was a lot of negative reaction.
I'm a bit surprised that
;
is required as a separator, rather than,
. Can you elaborate on that?
Well, we could in principle permit ,
elsewhere in traits, as far as I know, but we don't right now.
One complication I can see would be where
clauses:
trait Foo {
fn bar() where A: B + C, // <-- is this `,` terminating the list of where-clauses, or the item?
fn baz()
}
I guess it would make sense to flesh out the various syntax options more concretely. There is definitely a diversity of opinion on this topic. I think one particularly important knob is how they would support "shared-only" fields.
ok, I updated the main text to reflect various alternatives. Let me know if anything is unclear or different from how you would have expected it.
+1 for field
. If we then wanted to expand struct
definitions to allow for other kinds of items, you'd be able to use field
there as well.
@aturon
I have a pretty negative reaction to the let syntax, because it goes afield of both the meaning of the word "let", and any existing use of it in Rust. Traditionally, "let" (in mathematics and elsewhere) is used to bind a variable to a value.
I was thinking on this a bit more. I think part of the reason I sort of like let
is that, in an impl
specifically, it maps so well to the mathematical meaning (rather better than how we use it for local variables, where due to dtors etc it is better to think of it as a "slot" than as a "binding to a value that existed before", I think). But I can see that in traits it sort of "open-ended", and I guess that's what you are reacting to.
I am a bit nervous about field
-- and maybe let
too -- in that it somehow feels a bit verbose to me. I am thinking of patterns like https://github.com/nikomatsakis/fields-in-traits-rfc/issues/9, where all of your declarations are in traits.
I strongly prefer the struct
syntax, as in:
trait Foo {
struct {
bar: u32,
baz: u64,
}
}
For several reasons:
- The clean separation of data from code. I know Niko doesn't feel as strongly about this and has even suggested putting inherent methods inside struct definitions, but I think even if you don't value it that much, its become an iconic aspect of Rust's syntax.
- Conceptually, I prefer to think about this as "pattern abstraction" - defining a pattern that can be applied to multiple types. This pattern can then be employed as destructuring or as field access.
- Along those lines, I'd like to someday see refutable patterns available through traits as well:
trait Optional<T> {
enum {
Yes(T),
No,
}
}
To which you can then define a mapping which we will check for exclusivity & exhaustiveness.
I know Niko also has a different idea for how pattern abstraction could work inspired by extractor classes in Scala.
I suspect Niko's custom disjointness rules favor using struct
, or at least using braces to express disjointness, because multiple struct
field declaration can produce different families of disjoint fields, and we identify identically named fields in different struct
field declaration to express overlap.
If struct
is used, it would be good to keep it compatible with both structural records, declaring struct
s in trait
s and impl
s, and using struct
to declare fields in impl
s.
trait Foo {
struct { field: u64 } // "ordinary" field declaration
type Bar;
struct Baz<'a>(&'a Foo,Bar); // tuple struct declared in a trait
fn foo(&self) -> Baz;
fn bar(&self) -> { a: u64, b: u64 } { .. } // default method that returns a structural record
}
impl<T> Foo for T where T: Fooish {
struct { field = self.fooish }
struct { field, impl_field = self.notfooish } // field declaration in the impl
// field is added here to express disjointness between field, and impl_field
struct BazLike<'a>(...); // tuple struct declared in impl
type Bar = { x: Blah, y: Blah }; // structural record type assigned to associated type
...
}
It kinda looks like struct
might be compatible with all that, but this assume a structural record type is { .. }
not struct { }
, although that might work too. If so, this gives an argument in favor of struct
, although a contextual keyword like alias
or field
might still be clearer, even if it used the syntax fields { .. }
to express disjointness.
Having let this sit for a while, I think I've come around to the "anonymous" struct
syntax:
trait Foo {
struct { (mut? field: Type),* }
}
impl Foo for T {
struct { (field: value),* }
}
There would be at most one struct
section permitted in any given trait or impl, and (default) impls do not have to list all the fields.
There are some advantages:
- familiar syntax with fairly intuitive meaning
- "implementing types must be structs with fields rather like this"
- maybe scales up to finger-grained disjoint declarations
- though I suspect we'll want to handle that in a more general way, to also cover methods and things
- maybe scales up to some of the ideas that @withoutboats was putting forward about overloadable matching
It's not perfect -- in particular, I think we probably still want fields to be "read-only" by default, and hence the syntax between traits and structs is mildly asymmetric:
trait Foo {
struct {
mut f: u32
}
}
However, it seems better to me than inventing a keyword like field f: u32
. I'm having a hard time putting my finger on why, but that just doesn't feel very rusty to me. Perhaps it's the fact that it has no analogous syntax elsewhere in the language. I'm curious to hear if there are people who are very fond of field f: u32
-- perhaps I am mistaken here.
(That said, I personally still find let f: u32
quite nice, but I know there are also some strong objections to it.)
I think fields { ... }
expresses the meaning of the functionality better than struct { ... }
. As we are not composing data into a type. We are mapping fields from a struct to an associated type.
I was fond of field f: u32
, but that was partially because I wasn't fond of having struct {}
inside a trait declaration. If there is a struct {}
then I agree that it's better to have no keyword by default and accept the mut
weirdness.
I'm coming around to struct {}
in part because, as you say, it specifies that "implementing types must be structs", and I'd completely forgotten to consider enums and primitive types implementing traits the last few times I looked at these syntax options.
@lxrec
I'm coming around to struct {} in part because, as you say, it specifies that "implementing types must be structs", and I'd completely forgotten to consider enums and primitive types implementing traits the last few times I looked at these syntax options.
Note that we could eventually loosen this, if we get some way for (e.g.) enums to have common fields. But then we are saying "this trait acts like a struct" -- and, perhaps, implementing types must have some "struct-like subset" of them. So it still works.
I couldn't quite get the tone of your comment. Seems like you used to dislike struct { }
but now think maybe it makes sense?
I feel like we have to unblock on this point, in any case. Certainly before stabilization we could also revisit this question as we gain more experience, though it's the kind of thing you'd prefer to get right to start.
Would more finer-grained disjointness rules be expressed by grouping or something else? If grouping, then you could omit the struct
key word, use the existing ,
version, but eventually let { }
express disjointness.
trait Foo {
all(&mut self); // borrows everything
{
mut f: u32,
foo(&mut self); // borrows only f
}
{
mut f: u32,
mut g: String,
bar(&mut self); // borrows f and g mutably
}
{
g: String,
mut h: usize,
baz(&self); // borrows g and h immutably;
}
}
@burdges
Would more finer-grained disjointness rules be expressed by grouping or something else?
I shouldn't really have mentioned that, I think. It's hard to know what's the best design here and I don't really want to spend too much time thinking about it, because I see it as fairly orthogonal from this RFC. I think I agree with your broader point that struct
doesn't really have any advantages here, since any such design will want to consider methods and things too.
Well, I guess in this respect there is one slight advantage to struct
-- it conceptually lumps all the fields into one "item" in the list, and that feature 'interferes' with other items equally. That is, while you can borrow distinct struct fields simultaneously, borrowing any struct field mutably will prevent you from invoking &mut self
methods.
I'd think some well considered contextual keyword would be best "pedagogically", maybe not fields
if it becomes the disjointness for methods too, maybe words like members
or group
or selection
, not sure.
In fact, one could always separate methods into a different block with any syntax, like say fields { field1: T , .. } methods { fn method1(), .. }
, which maybe removes some pressure to figure out too much about methods right now.
You guys could go with struct
for now and either switch to a contextual key word before stabilization or else decide that something like struct { field: T , .. } impl { fn method(), .. }
would be okay if methods were really needed pre-2.0.
I would like to propose another option: "super type", analogy to "super trait".
struct Foo1 {
f1: u32
}
struct Foo2 {
f2: u32
}
trait Bar: Foo1 + mut Foo2 {
fn call(&self) {
self.f2 += 1;
let x = self.f1 + self.f2;
...
}
}
impl Bar for T {
struct Foo1 { f1 : member1 }
struct Foo2 { f2 : member2 }
fn call(&self) {
let x = self.member1 + self.member2;
...
}
}
I'm not saying it's better or not. This option seems not mentioned by anyone.
@F001 That might be a confusing name, as terminologically it would be a form of subtyping AFAIK.
instead of putting self.
can we put Self::
instead?
it's the same syntax that used to represent fields in #14
to support case #12 we can implement self
field on Any
trait
so we can put Self::self
or Any::self