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

Deriving options #8258

Closed
wants to merge 7 commits into from
Closed

Deriving options #8258

wants to merge 7 commits into from

Conversation

huonw
Copy link
Member

@huonw huonw commented Aug 3, 2013

This adds the infrastructure to allow #[deriving] to take options (closes #7229), e.g.

#[deriving(Eq(ignore(a)))]
struct Foo {
    a: int,
    b: uint
}

It also implements three options for the comparison traits test_order, ignore and reverse, e.g.:

#[deriving(Eq(test_order(y), ignore(z, w)),
           TotalOrd(reverse(x)))]
struct Foo {
    x: uint,
    y: uint,
    z: uint,
    w: uint
}

which means that the field y is tested first, z and w aren't tested at all (for Eq), and x is sorted in reverse order for TotalOrd.

For the comparison traits, the options are strictly checked; it is an error to:

  • specify anything other than those that the trait supports (all support test_order, Eq & Ord support ignore, and Ord & TotalOrd support reverse.)
  • specify fields that don't exist
  • use a format different to Trait(option(bar, baz))
  • use any of these options on enums or tuple structs: only structs with named fields.

(This also adds documentation to the manual about this.)

At the moment, all other traits emit a warning if any options are used (and ignore them).

(In general, all forms of options are supported, e.g. #[deriving(Foo="bar")] works too.)

@huonw
Copy link
Member Author

huonw commented Aug 3, 2013

r? @alexcrichton

large >= large;

small >= large;
large <= small;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these have assertions of their truthiness or falsiness?

@alexcrichton
Copy link
Member

I'm a little worried that this is a lot of code to support a not-too-common case. Is there precedent for having these options in the deriving syntax? All I know is that Haskell has deriving, but I know very little Haskell and a cursory search didn't come up with much.

I'm ok with implementing custom behavior myself, but I may not be a good sample of the opinion at large. Regardless, I'm not sure that test_order is completely necessary. I'd imagine that the common use-case of this would be for ignoring fields or inverting comparisons on fields, but I'd think that it'd be pretty rare you'd want to have one test_order for one trait and one for another (I could be wrong though).

{ // restrict the lifetime of these immutable borrows.
let mut check_valid = self.test_order.iter()
.chain_(self.ignore.iter())
.chain_(self.reverse.iter());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yay iterators!

@alexcrichton
Copy link
Member

As usual though, good clean code with good tests!

@brson
Copy link
Contributor

brson commented Aug 3, 2013

I guess I'll voice some opposition to this direction. Deriving doesn't need to be such a general mechanism. If one has special needs for how Eq is implemented, then it isn't that painful to write it by hand.

The linked issue provides little justification that this is warranted. This is another compiler-integrated feature that will need to be specced, but will it ever be used, and will the amount of use it gets justify the complexity? I'm not convinced.

huonw added 6 commits August 4, 2013 19:37
Previously it would call:

  f(sf1.cmp(&of1), f(sf2.cmp(&of2), ...))

(where s/of1 = 'self/other field 1', and f was
std::cmp::lexical_ordering)

This meant that every .cmp subcall got evaluated when calling a derived
TotalOrd.cmp.

This corrects this to use

   let test = sf1.cmp(&of1);
   if test == Equal {
      let test = sf2.cmp(&of2);
      if test == Equal {
        // ...
      } else {
        test
      }
   } else {
     test
   }

This gives a lexical ordering by short-circuiting on the first comparison
that is not Equal.
For example, in #[deriving(SomeTrait(foo, bar="baz", qux))], the bracketed
data will get passed to the SomeTrait implementation. It supports both the
list notation and the #[deriving(SomeTrait="a literal")] notation.
…al structs.

Example:

    #[deriving(Eq(test_order(y), ignore(z, w)),
               TotalOrd(reverse(x)))]
    struct Foo {
        x: uint,
        y: uint,
        z: uint,
        w: uint
    }

This allows several things to be controlled:
- `test_order` prioritises certain fields (e.g. the cheapest tests be placed
  first), i.e. y is tested first, and then x. This will not change the result of
  `Eq` & `TotalEq` impls, but can change `Ord` & `TotalOrd`
- `reverse` reverses the result of Ord & TotalOrd comparisons for the given fields
   (so it is as if x = 1 > x = 2 in the example above)
- `ignore` completely removes the fields from the comparison, e.g. `z` and `w` are
   not considered at all for the `Eq` impl of `Foo` above.

Only `Eq` and `Ord` accept `ignore` (since a `Total*` impl ignoring some fields
would not be total), and only `Ord` and `TotalOrd` accept `reverse`. All 4 traits
accept `test_order`.

Any other option, or using a syntax different to `#[deriving(Trait(opt(field1,
field2, ..)))]`, is an error. Using any of these options on an enum, or a tuple or
unit struct is also an error.
@huonw huonw mentioned this pull request Aug 4, 2013
@huonw
Copy link
Member Author

huonw commented Aug 4, 2013

I've split the general bug fixes into #8285, so this is now purely the controversial commits (syntax: pass options to #[deriving] implementations. onwards).

I'm addressing the comments now; and there's definitely no problem for this to be r-'d. I'm happy to simplify (e.g. removing test_order), if necessary; it just seems very annoying to sometime have a single field that needs to be ignored, and thus unable to use #[deriving] even though the rest is so mechanical & boiler-plate-y.

(FWIW, I'm pretty sure Haskell doesn't support anything like this for its deriving.)

@emberian
Copy link
Member

emberian commented Aug 7, 2013

I still like this. The more work the compiler can do for me, the less work I have to do, esp when adding fields etc.

@catamorphism
Copy link
Contributor

I agree with the concerns raised by @brson and @alexcrichton , so I'm going to close this.

osaut pushed a commit to osaut/rust that referenced this pull request Oct 31, 2013
Some general clean-up relating to deriving:
- `TotalOrd` was too eager, and evaluated the `.cmp` call for every field, even if it could short-circuit earlier.
- the pointer types didn't have impls for `TotalOrd` or `TotalEq`.
- the Makefiles didn't reach deep enough into libsyntax for dependencies.

(Split out from rust-lang#8258.)
@huonw huonw deleted the deriving++ branch December 4, 2014 02:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Extend #[deriving] to allow options to be specified
5 participants