paholg / dimensioned

Compile-time dimensional analysis for various unit systems using Rust's type system.
https://crates.io/crates/dimensioned
MIT License
300 stars 23 forks source link

Ergonomics #9

Closed paholg closed 8 years ago

paholg commented 8 years ago

I would like using dimensioned to be as painless as possible. It should feel like you're working with primitives, only they yell at you more when you make mistakes.

If you have any thoughts on the ergonomics of dimensioned, in regards to anything, please add them here!

type aliases

Currently, we have aliases that look like this:

pub type Meter = SI<P1, Z0, Z0, Z0, Z0, Z0, Z0>;

which requires you to use it like this:

fn length() -> Dim<Meter, f64> {
    5.7 * m
}

I would like to switch the aliases to look like this:

pub type Meter<V> = Dim<SI<P1, Z0, Z0, Z0, Z0, Z0, Z0>, V>;

so that the above function could be replaced with:

fn lenth() -> Meter<f64> {
    5.7 * m
}

I am not certain that this won't cause issues in other places (especially in defining derived units, which I'm currently working on as well), but barring that I'm not 100% sure it's a good idea.

It makes dimensioned types cleaner to view and easier to type.

It makes error messages more cryptic (they won't change, but they contain Dim which users will no longer need to deal with directly).

It also might make some things more confusing. I'm not sure exactly.

derived units

It is important for both unit systems and users of them to be able to conveniently define derived units. Right now, you can do

type Newton = SI<P1, P1, N2>;

but that isn't great. I have a macro in progress that can turn

type Newton = derived!(Meter * Kilogram / Second / Second);

into

type Netwon = Quot<Quot<Prod<Meter, Kilogram>, Second>, Second>;

but that has flaws as well, mainly due to the typechecker not doing what we want. Basically, Newton won't always act like it's SI<P1, P1, N2>, but instead will be treated as the associated type Output of those computations, even though the latter, once computed, is the former. Basically, this issue.

One solution is to use a build script to generate derived types, but that doesn't help users make their own, which should be as painless as possible. Possibly this can be solved with syntax extensions, but I have no familiarity there.

iliekturtles commented 8 years ago

I think changing the type aliases is a good idea. See my reply to #8. Derived units are going to need review whatever way they are defined. type Newton SI<P1, P1, N2> is as easy to mess up at derived!(Meter * Kilogram / Second / Second). If the most commonly used quantities are already defined within the library and are going through a code review process then ergonomics isn't as important since each library user wouldn't be asked to redefine Force/Newtons.

paholg commented 8 years ago

I'm working on the alias switch, and it's really nice for base units. You can even do things like

let x = Meter::new(3.0);

which I think is a big win.

The problem is it breaks down for derived units, due to Rust's issue of not normalizing associated types in time. Worst case scenario, we can go with a build script, but I'm gonna look into procedural macros and see if there's anything there that can save it.

paholg commented 8 years ago

I am restructuring dimensioned from the ground up. Most importantly, what used to be Dim<SI<Meter, Second, ...>, V> is now SI<A, V> where A is a type-level array of type-numbers.

This carries with it a few advantages.

One, when one defines a unit system, they are creating the outermost type, and so no one has to worry about orphan rules for unit systems they define. I consider this a big plus.

Two, it lets us use std::convert::From for conversions! I am very excited about this.

Here is an example of a conversion from a meter, kilogram, second system to a centimeter, gram, second system:

impl<V, Meter, Kilogram, Second> From<mks::MKS<V, tarr![Meter, Kilogram, Second]>>
    for CGS<Prod<V, f64>, tarr![Meter, Kilogram, Second]> where
    Meter: Integer, Kilogram: Integer, Second: Integer,
    V: Mul<f64>,
{
    fn from(other: mks::MKS<V, tarr![Meter, Kilogram, Second]>) -> Self {
        // Note we have to be a bit careful here because these unit systems are special and use
        // double the regular unit power, so that they can be represented with half integer
        // powers. E.g. The unit for area will really be `m^4`.
        let mfac = 100.0f64.powf(Meter::to_i32() as f64 / 2.0);
        let kgfac = 1000.0f64.powf(Meter::to_i32() as f64/ 2.0);
        // Both systems use seconds for time, so no need to do anything with it!
        // let sfac = 1.0f64.powi(Meter::to_i32());
        let fac = mfac * kgfac;

        CGS::new( other.value * fac )
    }
}

With the old system, I couldn't get any conversion to a level of usability that I was satisfied with. I can't think of any method of conversion that would be better than this way, so I see it as the biggest win from this restructuring.

The only real downside that I can think of is that pretty much the entire library is now contained in the make_units macro, which makes implementing it and will make maintaining it a bit more unpleasant. I don't see this as a big deal though.

You can see the current changes in the rmdim branch. You won't be able to run it, though, without local versions of typenum and generic-array, because it depends on an addition to typenum that hasn't yet been merged to master. That should happen soon, though.

Edit: fixed this!

paholg commented 8 years ago

I believe the issues here have generally been solved.