Is there a way to not need units? #2

Closed
dmcclean opened this Issue Feb 11, 2014 · 21 comments

Comments

Projects
None yet
2 participants
Collaborator

dmcclean commented Feb 11, 2014

It's possible to imagine a flavor of this library where the Variant concept is eliminated, there are only quantities, and units don't exist.

It seems like this would be simplifying and desirable, except for two things:

  1. Introduction/elimination of Dimensionless-ness.
    • Introduction can probably be fixed, perhaps more nicely at least for client modules which pervasively use dimensional-dk, by making a (Fractional a) => Fractional (Dimensionless a) instance and the corresponding fromRational function.
    • You could consider using another fromRational function instead, if you strongly dislike the Prelude numeric type hierarchy (which certainly leaves a lot to be desired). Rebindable syntax makes this pretty painless, but there is the question of which alternative hierarchy to use. You could also consider just making a non-type-class fromRational function for dimensionless, and using only that in dimensional-dk client modules, I suppose.
    • Once you do that, you don't need *~ and /~ anymore, because 6.3 * meter now promotes the 6.3 to (Fractional a) => Dimensionless a, and meter is (Num a) => Quantity DLength a.
    • But elimination is another question. Dividing by your (now just a quantity) unit would get you as far as, e.g. Dimensionless Double, but to get back to a raw Double you need another step. extractValue :: Dimensionless a -> a (or unD or something) is possible. But is the noise of that worth the benefits of getting rid of unit vs quantity?
    • Is the new coerce function already this elimination operator? I think it might be, because all the hypothetical extractValue function does is unwrap the newtype.
  2. Unit prefixes.
    • One might argue that it feels unhygienic to allow kilo and friends to apply to general quantities.
    • We maintain "Unacceptability of stand-alone prefixes". And just as now we sorta do and sorta don't have "Unacceptability of compound prefixes" (depending on whether you count composing them with . or parenthesees). But we lose "Unacceptability of stand-alone prefixes".

There are a lot of pros and cons, so it almost feels like a coin flip.

The fact that units might have an important static role to play (in constructing useful fixed-point quantities) is another thing to consider.

Owner

bjornbm commented Feb 20, 2014

Separating units and quantities was a very conscious decision, and one that I am quite happy with and feel has worked quite well in practice (YMMV). Converting quantities to values is performed, by necessity, very close to NIST guidelines (see 7.1 and 7.8) and I think this is a good thing.

A wart is the fixity of (*~), (/~) being identical with (*), (/), necessitating parens for compound units. I'd love a solution to that (there was once talks of more fine-grained fixity in GHC, but I don't think it went anywhere).

Collaborator

dmcclean commented Feb 20, 2014

Yeah, I was working on a branch last weekend that dropped the distinction and I couldn't quite get it to where I was happy with it. I'm still batting around a few ideas with it, though. I want to see what happens if you keep the distinction but merge (*) with (*~) etc.

One thing I discovered is that I think the difference between quantities and units might be that units have names. But in order for Dimensional to be a newtype, we have to drop the term-level information about what the unit names are. This is inconvenient when you want units (as opposed to just dimensions and quantities) to play some role in your program. (The only such role I can think of is user interaction. Maybe historical/legal record keeping?) I have a hackish hack that keeps the newtype for quantities but gets rid of it for units, but I think it is too much of a hack to survive the light of day.

A thing that I do like from the branch I've been working on is:

fromRational :: (Fractional a) => Prelude.Rational -> Dimensionless a
fromRational = Dimensional . Prelude.fromRational

I've only gotten to use it with one example client so far, though, so it may turn out to be more trouble than it's worth later. You might want to play around with it and with -XRebindableSyntax.

Owner

bjornbm commented Feb 20, 2014

I'm not sure how much value carrying around e.g. unit names would be, even in user interaction. As soon, as you combine units, do you really care about the original units or will you want e.g. joule instead or N*m, or Hz instead of s^-1? For record keeping needs there is always the option of a specialized a.

I'll close this issue for now. We can reopen if you arrive at something you think is a worthwhile change regarding quantities and units.

The combination of fromRational and -XRebindableSyntax is interesting and I'll try to exercise it in some of my code to see how it works out. I kind of tried to achieve the same effect before with a Num (Dimensionless a) instance, but it was a mess in practice. See here. But with your fromRational I guess the type inference problems with the Num instance are avoided. Let me know how it turns out in practice for you, and open a new issue once you are certain you like it!

Do you need to rebind fromInteger too in order to use e.g. 2 instead of 2.0?

@bjornbm bjornbm closed this Feb 20, 2014

Collaborator

dmcclean commented Feb 20, 2014

Agreed on all aspects of that plan.

You would need to rebind fromInteger as well if you want to use 2 instead of 2.0. But I found that I liked it better with just the fromRational because I could always tack on a .0 and it left the integers close at hand for counting things, indexing, and other things that are more "bookkeeping" than numerical.

Collaborator

dmcclean commented Feb 20, 2014

Oh, I should also say that the reason I thought about tracking unit names for composite units is that sometimes the units that people use are composite units (e.g. km^2, ft/s, mi/h, kW h, etc.). The intent wouldn't be to track this in the type system, or a newtype for bookkeeping would indeed be the solution. At the level of quantities you immediately forget the names and the path you took to get there, so that joules and newton-meters are indistinguishable, which is perfect. But if what you are actually manipulating is the units, I think it is helpful to track them.

On my list of missions is to build a parser that takes strings to unit names, a dictionary that takes unit names to actual Unit a values for some a, and something that glues them together along the lines of createUnit :: UnitName -> Maybe (Unit d a).

If it isn't named-ness, what do you see as the unit/quantity dividing line philosophically? Is it just that units are the gatekeepers to quantities because you use them to introduce/eliminate quantities.

Owner

bjornbm commented Feb 21, 2014

It is mainly the latter. As the NIST guide says:

The value of a quantity is its magnitude expressed as the product of a number and a unit, and the number multiplying the unit is the numerical value of the quantity expressed in that unit.

More formally, the value of quantity A can be written as A = {A}[A], where {A} is the numerical value of A when the value of A is expressed in the unit [A].

That Units have a term-level numerical value is an implementation detail, allowing to normalize the units of all Quantitys to the SI base units. I.e. the unit [A] of any Quantitys is implicit.

That is what led to the dividing line: I decided it would be convenient to normalize Quantitys, meaning that Units had to carry their scale factor w r t SI base units. Reusing the Dimensional newtype rather than having a separate type for Units made reuse of (*), (/), (^), etc convenient.

Collaborator

dmcclean commented Feb 21, 2014

I think there are two parts that feel weird to me about it.

The first is that you need a different operator for let width = 3.6 *~ meter than you do for let area = width * width or for let area = (3.6 *~ meter) * (3.6 *~ meter).

The second is that you are on your own with regard to managing the names, even though it turns out that the names have all the same operations (multiplication, division, exponentiation) as the units themselves do. And so you can have let raw = energyUsed /~ mega joule but not showIn (mega joule) raw where showIn :: (Show a) => Quantity d a -> Unit d a -> String.

The first thing is equivalent to relaxing the type of (*) (and friends) to (analogs of) (*) :: Dimensional v1 d1 a -> Dimensional v2 d2 a -> Dimensional (Combine v1 v2) (d1 * d2) a. (With the obvious type family that spells out how unit-ness vanishes when you mix in a quantity.) So maybe I will play around with that and see what gets worse.

The second thing would be way more challenging. It's incredibly straightforward, but only if you sacrifice the newtypeness of Quantity, if you try to keep the newtypeness it goes off into the weeds pretty quickly; I was messing with some prototypes of that yesterday.

Owner

bjornbm commented Feb 22, 2014

For clarity, could you give me an example of showIn at work? Is the type signature flipped?

Collaborator

dmcclean commented Feb 26, 2014

Indeed, it is much nicer when flipped, I hadn't gotten that far in my plan yet.

Check out this alternate universe: https://github.com/dmcclean/dimensional-dk/tree/named-units

Units and quantities still exist. And quantities are still a newtype. But (*) and (*~) have merged. (/~) has taken on a lesser role as just shorthand for a division and an unwrapping. Units carry names. You can multiply and divide units but not add them. You can use prefixes only on atomic units, so there is no such thing as kilo (meter / gram).

Collaborator

dmcclean commented Feb 27, 2014

Be sure to look at 311c8ef or newer, I made some braindead mistakes/omissions because I was working too quickly.

Owner

bjornbm commented Mar 3, 2014

How is this working out for you? Are you arriving at something you are happy with?

I see a bunch of classes/constraints but don't have a feeling for how it would work out in practice (IMO one of the great advantages of moving from fundeps to type familes is reducing the number of constraints, so I am worried this is a regression, but I haven't put it to the test).

The GM example obviously didn't change much, but how a bout library code where the dimensions are not concrete?

Just checking in to see if you're still pursuing this, in which case I'll take a closer look.

Collaborator

dmcclean commented Mar 3, 2014

I found it to be nice, but then moved my focus back to submitting pull requests for all the small changes.

I will merge all the latest stuff into my named-units branch and see if it still works. After which I should have some more comments. (And after which I can try it with the linear algebra library.)

For now:

You are correct that there are a lot of constraints. The nice thing about it from the perspective of library code is that once you specialize as far as Quantity d a you don't need to mess with constraints. The constraint-laden types only burden people who are trying to make functions that operate generically over units or quantities. But I can't think of much library code where you would want to do that (outside of dimensional-dk itself).

For example, see 416-426, where the types of mean and sum are unburdened by (irrelevant) constraints. Although, now that I look at it, I wasn't completely consistent in choosing that path. Look at 470-479 where I gave more (and probably excessively-) general types to (**) and atan2. Does anyone really need the ability to say atan2 mile (kilo meter) instead of atan2 (1.0 * mile) (1.0 * kilo meter)? Probably not, since any real invocation of atan2 probably uses arguments that were converted into Quantity form a long time ago, like xPos and yPos. For (**) it is even more egregious, since there is only one useful unit for DOne, and even it isn't that useful.

The extra phantom type that prevents you from writing kilo (micro meter) does complicate the type of something like showIn at 646-649. But here (at least) it seems like the power it achieves is worth it, and also unlikely to impact newcomers.

More to follow.

Collaborator

dmcclean commented Mar 4, 2014

I largely completed that merge.

I left out re-implementing siUnit in this parallel universe because it is slightly more complicated. It actually becomes the home for a lot of the reflection computation that is currently in show, because it has to put together not only the value 1, but also a name for the unit. That's actually an encouraging sign of being on the right track, I think, because show would become showIn siUnit!

I probably have some minor cleanup to do, but it is getting late.

Collaborator

dmcclean commented Mar 4, 2014

A collection of rocks and hard places for making that refactoring, though, because of the module structure:

  • Duplication
  • Mutual dependency between DK and DK.SIUnits
  • Moving definition of meter, gram, second, ampere, kelvin, mole, and candela to DK
  • Moving siUnit and show to DK.SIUnits
  • Making a DK.Internal module and importing this part of it to both DK and DK.SIUnits, but not re-exporting it from DK
  • Something else
Collaborator

dmcclean commented Mar 4, 2014

I am going to change this to be even more literal about unit names, so that you can have units like m/m if you want them, or m/s/s instead of m/s^2. This will get rid of the containers dependency and will actually be a bit more expressive.

Owner

bjornbm commented Mar 4, 2014

I don't have a problem with moving the SI base units to DK.

(**) and atan2 on units seems a bit crazy to me. In what situation would that make sense??

Collaborator

dmcclean commented Mar 4, 2014

Yeah, it doesn't make any sense, I will change it. I think I was translating mechanically and I carried the pattern from (*) and such where it matters on to (**) and atan2 that should be only on quantities.

I will fix that and move the SI base units to DK, and make the revised siUnit tonight after dinner.

Collaborator

dmcclean commented Mar 4, 2014

For now (since this whole thing is speculative anyway) I am going to move the prefixes too. The reason is so that the code in the show instance / siUnit function can have access to the kilo gram and not just the gram.

Collaborator

dmcclean commented Mar 5, 2014

That actually worked out quite nicely!

Arguably one drawback is that, because showIn has a Fractional context, as a result so does the Show instance for Quantity and Unit.

I think I would argue that we should go through and change all the Num contexts to Fractional anyway. Using the library with integral types takes an assumption on what units are used for representation, limits you to only a handful of operations, and so forth. And the Fixed fixed-point types, which it would be nice to be able to use, do have Fractional instances.

That said, the drawback is there for what it's worth.

Owner

bjornbm commented Mar 5, 2014

I don't have a problem with the Show instance having a Fractional constraint. I'm not sure that means we can't keep the Num context for e.g. (+), even though I agree that the generalization it is mostly useless.

I think it is understood, but I'll clarify that what I'm saying is that these things would not stop me from accepting a patch. I'm not (yet) saying I will accept your named-units branch.

Collaborator

dmcclean commented Mar 5, 2014

Understood. It is a pretty gigantic change, I'm not even sold on it myself yet. Needs more actual use to see where the problems end up.

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