-
Notifications
You must be signed in to change notification settings - Fork 1.7k
rust: create types module with primitives like Step
#4342
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
Conversation
Summary: We’d defined a `Step` [newtype] in the `reservoir` module, but that type will be useful more broadly. We thus move it into a new `types` module, and define `WallTime` and `Tag` siblings, which we’ll need shortly. [newtype]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html Test Plan: Most behavior is just `#[derive]`d; unit tests included for the rest. wchargin-branch: rust-types-module wchargin-source: b0f5475b247ef8f9cb8bf2e0bf94d2b648b8009e
| .unwrap_or_else(|| unreachable!("{:?} <> {:?}", &self, &other)) | ||
| } | ||
| } | ||
| impl Eq for WallTime {} |
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.
For my education, is this any different than #[derive(Eq)]?
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 does the same thing that derive(Eq) would do, since Eq itself has
no methods (it’s a marker interface). But you’re only permitted to
derive(Eq) on types all of whose fields are also Eq. In this case,
we have a field of type f64, which is not Eq (because of NaN), so
we cannot use derive(Eq):
$ git diff -U1
diff --git a/tensorboard/data/server/types.rs b/tensorboard/data/server/types.rs
index 59a764504..755329912 100644
--- a/tensorboard/data/server/types.rs
+++ b/tensorboard/data/server/types.rs
@@ -26,3 +26,3 @@ pub struct Step(pub i64);
/// Wall times represent floating-point seconds since Unix epoch. They must be finite and non-NaN.
-#[derive(Debug, PartialEq, PartialOrd, Copy, Clone)]
+#[derive(Debug, PartialEq, PartialOrd, Copy, Clone, Eq)]
pub struct WallTime(f64);
@@ -51,3 +51,2 @@ impl Ord for WallTime {
}
-impl Eq for WallTime {}
$ cargo check
error[E0277]: the trait bound `f64: std::cmp::Eq` is not satisfied
--> types.rs:28:21
|
28 | pub struct WallTime(f64);
| ^^^ the trait `std::cmp::Eq` is not implemented for `f64`
|
Instead, impl Eq for WallTime {} is our explicit way of declaring that
our type really does have a total equivalence relation, even though the
types of its fields do not.
|
|
||
| // Wall times are totally ordered and have a total equivalence relation, since we guarantee that | ||
| // they are not NaN. | ||
| #[allow(clippy::derive_ord_xor_partial_ord)] // okay because it agrees with `PartialOrd` impl |
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.
reading this, it looks like PartialOrd can never disagree with Ord. If so, what is this derive_ord_xor_partial_ord really enforcing?
After reading a bit more about this https://rust-lang.github.io/rust-clippy/master/index.html#derive_ord_xor_partial_ord, why is the impl Ord
block required when already given #[derive(PartialOrd)]?
(for my education of course)
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.
Correct: the PartialOrd and Ord implementations for a given type
must agree, else many operations are undefined. (Not the scary kind of
“undefined behavior”; it’s just a contract violation on basically every
API that cares about orderings.)
This line disables a Clippy lint, whose docs you’ve linked to. I think
that the purpose is to flag code like this:
#[derive(PartialOrd)]
struct BackwardInt(i64);
impl Ord for BackwardInt {
fn cmp(&self, other: &Self) -> Ordering {
// Our type is ordered backward!
self.0.cmp(&other.0).reverse()
}
}This Ord implementation is fine by itself, and you might think that
#[derive(PartialOrd)] would implement PartialOrd for our type by
delegating to its Ord. But in fact it would actually delegate to the
PartialOrds of its fields, which would make the PartialOrd and Ord
inconsistent in this case.
In our case, the derived PartialOrd is consistent with the manual
Ord (which you can tell since cmp just calls partial_cmp), so I
suppressed the lint error. We could write it out—
impl PartialOrd for WallTime {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}—but I didn’t see much reason to do that.
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.
Here’s a topical upstream request to teach the lint to recognize this
safe pattern:
rust-lang/rust-clippy#6219
| m.insert(Tag("accuracy".to_string()), 1); | ||
| m.insert(Tag("loss".to_string()), 2); | ||
| // We can call `get` given only a `&str`, not an owned `Tag`. | ||
| assert_eq!(m.get("accuracy"), Some(&1)); |
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.
Super noob question: What would &1 mean? Is it referring to reference to an i32 1? If so, is it stable and refers to the same value to L81?
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.
What would
&1mean? Is it referring to reference to an i321?
Yep! (where the i32 part is implied from type-checking)
If so, is it stable and refers to the same value to L81?
Nope! It’s like:
m.insert(Tag("accuracy".to_string()), 1); // line 81
let tmp = 1;
assert_eq!(m.get("accuracy"), Some(&tmp)); // line 84
// `tmp` is implicitly dropped hereWe can’t get a reference to the 1 from line 81; not only is it out of
scope, it probably never had a stack location at all (would be passed in
a register for this signature). But that’s okay, because equality on
references is defined in terms of equality on the underlying values:
In addition, the comparison operators transparently defer to the
referent's implementation, allowing references to be compared the same
as owned values.
You can get reference equality semantics by casting to pointers. This
is safe: it’s only dereferencing pointers that’s unsafe. But I’ve
never had to use this, even once. Value equality is basically always
what you want.
wchargin
left a comment
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.
Thanks for the quick review and the questions!
| .unwrap_or_else(|| unreachable!("{:?} <> {:?}", &self, &other)) | ||
| } | ||
| } | ||
| impl Eq for WallTime {} |
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 does the same thing that derive(Eq) would do, since Eq itself has
no methods (it’s a marker interface). But you’re only permitted to
derive(Eq) on types all of whose fields are also Eq. In this case,
we have a field of type f64, which is not Eq (because of NaN), so
we cannot use derive(Eq):
$ git diff -U1
diff --git a/tensorboard/data/server/types.rs b/tensorboard/data/server/types.rs
index 59a764504..755329912 100644
--- a/tensorboard/data/server/types.rs
+++ b/tensorboard/data/server/types.rs
@@ -26,3 +26,3 @@ pub struct Step(pub i64);
/// Wall times represent floating-point seconds since Unix epoch. They must be finite and non-NaN.
-#[derive(Debug, PartialEq, PartialOrd, Copy, Clone)]
+#[derive(Debug, PartialEq, PartialOrd, Copy, Clone, Eq)]
pub struct WallTime(f64);
@@ -51,3 +51,2 @@ impl Ord for WallTime {
}
-impl Eq for WallTime {}
$ cargo check
error[E0277]: the trait bound `f64: std::cmp::Eq` is not satisfied
--> types.rs:28:21
|
28 | pub struct WallTime(f64);
| ^^^ the trait `std::cmp::Eq` is not implemented for `f64`
|
Instead, impl Eq for WallTime {} is our explicit way of declaring that
our type really does have a total equivalence relation, even though the
types of its fields do not.
|
|
||
| // Wall times are totally ordered and have a total equivalence relation, since we guarantee that | ||
| // they are not NaN. | ||
| #[allow(clippy::derive_ord_xor_partial_ord)] // okay because it agrees with `PartialOrd` impl |
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.
Correct: the PartialOrd and Ord implementations for a given type
must agree, else many operations are undefined. (Not the scary kind of
“undefined behavior”; it’s just a contract violation on basically every
API that cares about orderings.)
This line disables a Clippy lint, whose docs you’ve linked to. I think
that the purpose is to flag code like this:
#[derive(PartialOrd)]
struct BackwardInt(i64);
impl Ord for BackwardInt {
fn cmp(&self, other: &Self) -> Ordering {
// Our type is ordered backward!
self.0.cmp(&other.0).reverse()
}
}This Ord implementation is fine by itself, and you might think that
#[derive(PartialOrd)] would implement PartialOrd for our type by
delegating to its Ord. But in fact it would actually delegate to the
PartialOrds of its fields, which would make the PartialOrd and Ord
inconsistent in this case.
In our case, the derived PartialOrd is consistent with the manual
Ord (which you can tell since cmp just calls partial_cmp), so I
suppressed the lint error. We could write it out—
impl PartialOrd for WallTime {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}—but I didn’t see much reason to do that.
| m.insert(Tag("accuracy".to_string()), 1); | ||
| m.insert(Tag("loss".to_string()), 2); | ||
| // We can call `get` given only a `&str`, not an owned `Tag`. | ||
| assert_eq!(m.get("accuracy"), Some(&1)); |
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.
What would
&1mean? Is it referring to reference to an i321?
Yep! (where the i32 part is implied from type-checking)
If so, is it stable and refers to the same value to L81?
Nope! It’s like:
m.insert(Tag("accuracy".to_string()), 1); // line 81
let tmp = 1;
assert_eq!(m.get("accuracy"), Some(&tmp)); // line 84
// `tmp` is implicitly dropped hereWe can’t get a reference to the 1 from line 81; not only is it out of
scope, it probably never had a stack location at all (would be passed in
a register for this signature). But that’s okay, because equality on
references is defined in terms of equality on the underlying values:
In addition, the comparison operators transparently defer to the
referent's implementation, allowing references to be compared the same
as owned values.
You can get reference equality semantics by casting to pointers. This
is safe: it’s only dereferencing pointers that’s unsafe. But I’ve
never had to use this, even once. Value equality is basically always
what you want.
Summary:
We’d defined a
Stepnewtype in thereservoirmodule, but that typewill be useful more broadly. We thus move it into a new
typesmodule,and define
WallTimeandTagsiblings, which we’ll need shortly.Test Plan:
Most behavior is just
#[derive]d; unit tests included for the rest.wchargin-branch: rust-types-module