Denominated Values - Part numeric and symbolic.

Well as long as "how I spent my summer vacation" is an interesting topic, I've got one.

I've recently been working on/thinking about a programming language type that I haven't seen implemented before. I guess "Denominated values" is the best name for it.

A denominated value is a real number, plus a quasi-symbolic representation of what that number denominates - meters, hectares, seconds, joules, etc etc etc.

The semantics are just what you'd expect: Multiply two denominated values together and you get another denominated value. Divide a length by a time and you get a speed. Divide a speed by a time and you get an acceleration. Multiply a distance times a distance and you get an area. Add two distances and you get a distance. Add a distance to a time and you get an error. Which is the whole point, because the idea is error checking.

Most of the error checks can be free for most compile-able languages - The denomination semantics exist in compilation, but can get boiled out of the executable (except debug versions of the executable). If the program passes denomination semantics during compilation it doesn't need to check again, especially not on every single operation.

I was remembering college physics where misunderstanding the problem often led to doing math and realizing I was trying to do some mathematical operation on values whose denominations were incompatible under that operation. Or when the answer came out with a value of the wrong denomination. Or when I blew a unit conversion.

Of course denominations coming out right is no guarantor of correctness. Consider the popular rule "71 divided by annual percent interest rate equals doubling time." This happens to come out fairly close for small interest rates, but is not a mathematically correct construction and fails hard the further out of its range you get. This is a mistake that someone would make, because they probably learnt it when they were ten and never realized that the mathematical reasoning is fundamentally wrong. This is the kind of mistake we would not want to make.

But it comes out correct in denomination. interest rate is in {thing per time} per {thing}, thus the unit {thing} cancels out and interest rate turns out to be denominated in {inverse time}. So 71 (a unit-less value) is divided by a value in {inverse time} and yields a value denominated in {time}, which is exactly how the answer is interpreted. You can assign this wrong answer value to a denominated variable and never know there was a problem - you print it out in years and it "looks right."

And of course correct operations on denominated values do not necessarily guarantee us correct units. It guarantees us Denominated Values that *CAN* be expressed in the correct units, but are still prone to humorous errors. Even if we have done everything right, some values come out in {inverse time} and can be expressed as interest rates, angular velocities, or frequencies, and whatever units you tell the system to express it in, any inverse-time denominated value will express without an error because those three things are the same fundamental units. They shouldn't be, and I don't really know what to do about that.

Implementation details: The constructor takes real numbers and construct unit-less denominated values, and has a matching 'conversion' that gets a real number if and only if the denominated value being converted is also unit-less.

so you can do things like this.

// implicit promotion of reals to unit-less allows DV*DV implementation to be used producing a DV answer.
// but since INCH is a length, addition will fail giving a NaDV unless foo is also a length.
// it doesn't matter what units were used when foo was assigned; if it's a length, inches will add to it correctly.
answer = 2.0*INCH + foo
...
// implicit conversion from unit-less DVs to reals gives NAN if the argument isn't unit-less.
// It doesn't matter what units were used when any DVs used in the calculation were created;
// if it's a length, this expresses it in inches.
print ("answer is " + answer/INCH + "inches.")

The simple implementation so far is that the number is a 64-bit double and the denomination is an array of 8 bytes. The denominations, and all operations and tests on them, are #ifdef'd to exist in debug mode only. The first byte of denomination is devoted to order-of-ten magnitude, -127 to +127. The remaining 7 bytes are each devoted to the power of one fundamental unit, allowing the fundamental units to have powers from -127 to 127. (I am not using #FF. During implementation it signified overflow).

Addition and Subtraction requires that the unit powers are equal and increase or reduce one of the values by the difference in magnitude before doing the operation. Multiplication and Division adds or subtracts every field of the denomination, including the Magnitude.

Internally values are represented in metric units, with various unit constants like INCH being denominated values representing the metric equivalent of one of that unit. So when someone creates a denominated value using 2*INCH they get the same denominated value as if they'd created it using 5.08*CENTIMETER. And there are also a lot of unit constants that use more than one of the denomination powers (WATT has a +1 power for joules and a -1 power for seconds for instance).

I've created accessors that get the numeric value as a standard real number regardless of whether the denomination is unit-less, or which get the denomination as a 64-bit integer. I'm not sure this was a good idea. I seem to get sucked into using them to do "denomination casts" whose value or hazard I can't really guess.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Joules vs. newton-meters, length vs. width vs. height

Joules measure work, newton-meters measure torque. Unfortunately they are the same unit. By the same token, length, width, and height are all meters, but you don't want to add them up. (Except when you are sending a package, in which case you do.) In general, though, you don't want to identify units with measurements.

Units

This seems related to the units work in F# and other languages.

I believe John Cowan means to say that you shouldn't identify dimensions with units, in which case I agree. His comment provokes me to realize that I've seen prior work on units, but I can't think of prior work on units that addressed dimensions as well, and this seems like an obvious pairing. That said, I didn't read any of the units work with care.

I think "denominated" as you describe it is essentially what LISP refers to as "ratio". It is helpful, but not computationally essential, if rational results are normalized by removing the GCD from numerator and denominator. Stroustrup plays with a C++ implementation here.

Ratios? No.

Ratios are purely numeric. They're just a way to represent numbers that aren't precisely representable in binary floating-point, like one-third or one-tenth.

I'm talking about something else: a number combined with information about what basic kind of thing (distance, area, volume, speed, etc) the number denotes. Basically it's a type to provide a little more error checking on calculations in physics and mechanics.

If the denomination comes up wrong (in the wrong units) or if we try to do an operation on denominations incompatible under that operation (like adding an area to a velocity) then we know that the calculation we're doing is the wrong calculation. There's an error somewhere and when it's fixed the program will compile without a denominated-value error.

I'm just remarking that the calculated denominations are often ambiguous in a human context. The idea that an interest rate is the same "denomination" as an angular velocity may be mathematically correct but it's weird.