Skip to content
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

syntax for declaring fields #3

Open
nikomatsakis opened this issue May 25, 2017 · 24 comments
Open

syntax for declaring fields #3

nikomatsakis opened this issue May 25, 2017 · 24 comments

Comments

@nikomatsakis
Copy link
Owner

nikomatsakis commented May 25, 2017

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 with let syntax, but does it feel weirder in some way? I guess not.
@burdges
Copy link

burdges commented May 25, 2017

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 traits, but I suppose trait Foo { enum { f: T, g: U } ... } works no mater the route chosen for fields, so no problem there.

@nikomatsakis
Copy link
Owner Author

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.

@aturon
Copy link

aturon commented May 25, 2017

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?

@glaebhoerl
Copy link

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.)

@burdges
Copy link

burdges commented May 25, 2017

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.

@MoSal
Copy link

MoSal commented May 25, 2017

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;
}

@nikomatsakis
Copy link
Owner Author

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()
}

@nikomatsakis
Copy link
Owner Author

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.

@nikomatsakis nikomatsakis changed the title let syntax for declaring fields syntax for declaring fields May 25, 2017
@nikomatsakis
Copy link
Owner Author

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.

@aturon
Copy link

aturon commented May 25, 2017

+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.

@nikomatsakis
Copy link
Owner Author

nikomatsakis commented May 26, 2017

@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 #9, where all of your declarations are in traits.

@withoutboats
Copy link

withoutboats commented May 26, 2017

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.

@burdges
Copy link

burdges commented May 26, 2017

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 structs in traits and impls, and using struct to declare fields in impls.

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.

@nikomatsakis
Copy link
Owner Author

nikomatsakis commented Jun 6, 2017

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.)

@MoSal
Copy link

MoSal commented Jun 6, 2017

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.

@Ixrec
Copy link

Ixrec commented Jun 6, 2017

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.

@nikomatsakis
Copy link
Owner Author

@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.

@burdges
Copy link

burdges commented Jun 8, 2017

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;
    }
}

@nikomatsakis
Copy link
Owner Author

@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.

@nikomatsakis
Copy link
Owner Author

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.

@burdges
Copy link

burdges commented Jun 9, 2017

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.

@F001
Copy link

F001 commented Jan 23, 2018

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.

@ron-wolf
Copy link

@F001 That might be a confusing name, as terminologically it would be a form of subtyping AFAIK.

@djdisodo
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants