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

Length and ScaleFactor with statically-checked units #35

Merged
merged 8 commits into from May 28, 2014

Conversation

@mbrubeck
Copy link
Contributor

mbrubeck commented May 15, 2014

For servo/servo#2226. See code comments for details.

@pcwalton
Copy link
Contributor

pcwalton commented May 15, 2014

I'm confused as to why this uses phantom types. I'd think it would be simpler to just use newtype wrappers, like Au does today.

@pcwalton
Copy link
Contributor

pcwalton commented May 15, 2014

In particular I'm a bit concerned about the complexity of the trait signatures here.

@SimonSapin
Copy link
Member

SimonSapin commented May 15, 2014

Looks good to me. (r=me modulo @pcwalton’s concerns.)

@mbrubeck
Copy link
Contributor Author

mbrubeck commented May 15, 2014

I'm confused as to why this uses phantom types. I'd think it would be simpler to just use newtype wrappers, like Au does today.

My earlier drafts used newtypes, but I found that it made it harder to write generic code and ended up requiring a lot more boilerplate. The current design allows all of the implementation to be generic, so adding a new unit requires only a single line of code. (See servo/servo#2444 for some examples.) I'm still pretty new to Rust, so maybe I was missing some possibilities. I'll try exploring this avenue more.

One thing that the phantom types helped with was writing generic code to cast between types like Length<DevicePixel, int> to Length<DevicePixel, f32>, where the unit is the same but the representation is different.

Au is hard-coded to an i32 representation. This seems bad because it would lead to an explosion of newtypes like struct DevicePixel(f32) and struct DevicePixelInt(i32), each requiring its own trait implementations.

Even if we parameterize the representation (struct DevicePixel<T>(T)), we'd need at least per-unit trait implementations, and possibly additional ones like impl IntLength for DevicePixel<int>.

In particular I'm a bit concerned about the complexity of the trait signatures here.

If the concern is about readability, we can use typedefs to hide the full type signatures of commonly-used types from most users of those types.

@pcwalton
Copy link
Contributor

pcwalton commented May 15, 2014

I'm confused…why do we want DevicePixel to be sometimes in floats and sometimes in ints? Wouldn't it be simpler to have just one machine representation?

@mbrubeck
Copy link
Contributor Author

mbrubeck commented May 15, 2014

I'm confused…why do we want DevicePixel to be sometimes in floats and sometimes in ints? Wouldn't it be simpler to have just one machine representation?

Possibly. DevicePixel coordinates, for example, are used as framebuffer indices, so it really makes sense for them to be uint. However, we may sometimes need a signed type to represent differences between coordinates. And we need to convert them to f32 at least as an intermediate step to multiply them by f32 scale values. And we need to accept DevicePixel coordinates as f64 from GLFW and as c_int from GLUT, and pass them as i32 to Azure.

We can definitely handle all of those cases by simply dropping typed units before casting to any other representation (and applying new units afterward as appropriate), though I think we can catch more errors and simplify the code if the values get tagged with units at the earliest point possible, remain tagged through any casts and calculations, and become untagged only at the last possible point before being passed to external code.

@pcwalton
Copy link
Contributor

pcwalton commented May 15, 2014

Could we perhaps use macros to reduce the boilerplate of defining new newtype wrappers?

@mbrubeck
Copy link
Contributor Author

mbrubeck commented May 15, 2014

My earlier drafts used newtypes, but I found that it made it harder to write generic code and ended up requiring a lot more boilerplate.

To make this more concrete: When using newtypes, I had to reimplement Add/Sub/Mul/Div/etc. for every single type. Length<Unit, T> lets me do this once in in rust-geom. This means that adding a new unit is now one line instead of around 50 lines of code.

In cases where you do want to enforce a single machine representation (and therefore don't need any type params), then with just two extra lines you can just add a public alias to a private type:

enum DevicePixelPrivate {}
pub type DevicePixels = Length<DevicePixelPrivate, uint>;
pub fn DevicePixels(x: uint) -> DevicePixels { Length(x) }

To the user of this code, it looks just like a newtype struct:

let x: DevicePixels = DevicePixels(1);

Could we perhaps use macros to reduce the boilerplate of defining new newtype wrappers?

I expect so, but I'm curious what the benefits would be versus the above. And it would still lose the ability to safely cast between representations while automatically keeping the same units, which I still believe is beneficial.

@pcwalton
Copy link
Contributor

pcwalton commented May 15, 2014

It just feels like a lot of type-level machinery to do something simple. Long type signatures have a significant readability cost.

@brendanzab
Copy link
Member

brendanzab commented May 15, 2014

What we really need is a separate units of measure library with compile-time dimensional analysis. Perhaps with macros for defining new types. I am ok with phantom types when they are used in the appropriate places, but this doesn't seem like it.

@metajack

This comment has been minimized.

Copy link

metajack commented on length.rs in 195991c May 19, 2014

This should probably say u64 instead of uint64

Also adds missing `impl Zero` to make tests pass, and fixes a typo in a comment.
length.rs Outdated
pub struct Length<Unit, T>(pub T);

// *length
impl<Unit, T> Deref<T> for Length<Unit, T> {

This comment has been minimized.

@brendanzab

brendanzab May 19, 2014

Member

I am a little concerned about this. This would cause unpredictable results of methods impled on T. I would rather an explicit .get() or .unwrap() method to properly enforce type safety.

This comment has been minimized.

@mbrubeck

mbrubeck May 20, 2014

Author Contributor

I agree; I'll change this to an explicit method. get or unwrap are both good names; I'm also considering val or value.


// Convenient aliases for Point2D with typed units

pub type TypedPoint2D<Unit, T> = Point2D<Length<Unit, T>>;

This comment has been minimized.

@brendanzab

brendanzab May 19, 2014

Member

Why can't we just enforce that points must have length units?

This comment has been minimized.

@SimonSapin

SimonSapin May 21, 2014

Member

We could, but we’d have to switch all of Servo in one go to typed points. This PR so far is backwards-compatible.

pub struct ScaleFactor<Src, Dst>(pub f32);

// *scale
impl<Src, Dst> Deref<f32> for ScaleFactor<Src, Dst> {

This comment has been minimized.

@brendanzab

brendanzab May 19, 2014

Member

Again, concerned about the possibimities of auto-deref here.

/// let mm_per_inch: ScaleFactor<Inch, Mm> = ScaleFactor(25.4);
///
/// let one_foot: Length<Inch, f32> = Length(12.0);
/// let one_foot_in_mm: Length<Mm, f32> = one_foot * mm_per_inch;

This comment has been minimized.

@brendanzab

brendanzab May 19, 2014

Member

This is cool 👍

@brendanzab
Copy link
Member

brendanzab commented May 19, 2014

After re-reading this I think I am warming to the idea.

/// let one_foot_in_mm: Length<Mm, f32> = one_foot * mm_per_inch;
/// ```
#[deriving(Clone, Decodable, Encodable)]
pub struct ScaleFactor<Src, Dst>(pub f32);

This comment has been minimized.

@brendanzab

brendanzab May 19, 2014

Member

Is there a way of making the wrapped type generic too?

This comment has been minimized.

@mbrubeck

mbrubeck May 20, 2014

Author Contributor

Yes, it would be straightforward to make this generic; the only drawback would be a bunch more type parameters throughout the implementation. I'm inclined to make this change, in order to make the rust-geom library as general as possible, though I'm also curious whether Servo could do without it. (For comparison, Gecko's ScaleFactor uses a hard-coded single-precision float.)

@mbrubeck
Copy link
Contributor Author

mbrubeck commented May 28, 2014

I think this is ready to land. r? @metajack

@metajack

This comment has been minimized.

Copy link

metajack commented on scale_factor.rs in b7e88ba May 28, 2014

Can this not be #[deriving(Clone)]?

@metajack
Copy link
Contributor

metajack commented May 28, 2014

@mbrubeck One small question, otherwise looks good.

@metajack

This comment has been minimized.

Copy link

metajack commented on b7e88ba May 28, 2014

r+

metajack added a commit that referenced this pull request May 28, 2014
Length and ScaleFactor with statically-checked units
@metajack metajack merged commit 87d9de1 into servo:master May 28, 2014
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

5 participants
You can’t perform that action at this time.