Design input for units library

Omg, so many replies. Apologies, I was unable to check these. Anyways, during our last iteration, we had implemented a small "proof-of-concept" (It obviously needs some reworks). I just pushed whatever code we had to a repository. I also do realize there are a few missing overloads in there.

Now, for the naming discussion, a review of libraries in other languages reveals they use unit extensively as well. So, I guess it should be fine.

Libraries we took a look at:
C++ - GitHub - mpusz/units: A compile-time enabled Modern C++ library that provides compile-time dimensional analysis and unit/quantity manipulation.
Rust - GitHub - iliekturtles/uom: Units of measurement -- type-safe zero-cost dimensional analysis (I understand the design of Rust is nowhere close to Chapel's, but it was worth having a look at)
Python - Pint: makes units easy — pint 0.18 documentation

think that, like traditional C++, we'd also only get errors upon post-resolution instantiations of operators on such types today, though we're pursuing an interface concept that, like C++, would aim to do the type checking more proactively. But it would still require an explicit instantiation at some point to get right.

It's not that hard to actually do basic parametric polymorphism. Then there are two ways to apply constraints: a kinding system, and type classes. Felix does both. C++ concepts should have been Haskell type classes.

I think your biggest problem is actually notation, i.e. grammar. At least to start you don't want to break the existing system of generics, even if they're flawed. For functions, I'd be starting with purely parametric polymorphism which means NO operations are allowed on values dependent on a type parameter unless the operation is passed in as an argument except for moving, copying, and assigning etc.

In fact even that extreme limitation is not strong enough ... because you also have substructural typing, that is, uniqueness types, and borrowing. Consider this (excuse Felix notation please):

fun diag[T] (x:T)=> x,x;

This should type check, but what happens if you pass a uniquely typed entity? They cannot be copied, only moved. So actually in Felix, the default kind for a type variable is TYPE which means sharable types and excludes unique types. Here:

fun swap[T:LINEARTYPE, U:LINEARTYPE] (xT,y:U) => y,x;

works for any type, since it only need to move values. The kind LINEARTYPE must be specified to allow the function to accept unique types.

That was all fine .. until I added the kind BORROWED to the system. :slight_smile:
Now I have to check that you cannot pass borrowed values to this function because borrowed values must be forgotten, and both diag and swap return the values instead of forgetting them. The need to do borrowing was always a problem, and in fact reading the Chapel docs inspired me to figure out how to do it.

Chapel already has a lot of the capabilities to do things properly in the compiler. It's actually the surface syntax that's more of a problem .. in most languages if you can get the syntax right implementing it isn't too hard.

I'm going to reply to a few points here.

Regarding the term "unit" I would agree that this term is in common use for "unit of measure" and I think it's likely that this usage is more common than the abstract algebra one. My own suggestion is merely that we not name the type for a number-with-a-unit as "unit" because I think that's confusing (since a "unit" would be e.g. meters but 10 meters is something else - a number with a unit). (Edit: quantity sounds just fine to me for this). In particular, calling the library "units" sounds reasonable to me.

Responding to @skaller - I'm never sure if I have the PL jargon straight but I believe that our work on "interfaces" / "constrained generics" is implementing the features you are asking after. See the Interfaces section in https://chapel-lang.org/releaseNotes/1.24/04-ongoing.pdf or the older proposal here: chapel/2.rst at main · chapel-lang/chapel · GitHub .

Additionally even with "traditional" generics, we have improved upon the point-of-instantiation rule from C++. See the Point of Instantiation section in https://chapel-lang.org/releaseNotes/1.23/01-language.pdf . We would still like the interfaces/constrained generics for better error messages (in particular, to indicate whether it is a use of a generic library or the generic library itself with an error) but I don't know of any severe problems with the point-of-instantiation design we have today.

For @AsianIntel - I think my main feedback on the code you posted is to suggest again that records are more reasonable for this than classes are - hopefully Brad's record example above is useful to you (the code sample starting with enum unitType). You can still have a helper record for what you have today in class unit but you will need to include it as a field rather than inherit from it. Additionally I looked at mass.chpl a bit in your implementation and I'm not quite understanding why the mass value has both compile-time elements (from the unit parent class) and run-time elements that are related to the unit (coefficient and constant). In particular I would expect that coefficient and constant can be param as well.

But it is somewhat likely that I am missing something here. I'm trying to understand why you are using classes. Do you have a requirement to e.g. create an array of units of a variety of types? I am imagining that meters and feet should be different types in the type system and that arrays of both meters and feet would not be needed.

1 Like

What if we do something like this, perhaps better named:

// the user can introduce any number of these:
record meters { param power: int; }   // denotes meter^power
record seconds { param power: int; }  // denotes seconds^power
... etc. ...

// 'valWithUnit' ties them together:
record valWithUnit { var value: real; type units; }

// use it like this:
var acceleration: valWithUnit( /* units = a tuple of unit types: */ (meters(1), seconds(-2)) );

To multiply two measurements, we need to combine the units of two valWithUnit types. This could be done using Reflection and walking over the components of the tuple types. I suspect we will need to add some simple functionality that Reflection currently does not have.

With slightly more work, we can implement a primitive in the compiler that combines two record types like:

record units1 { param meters: int; param kilos: int; val: real; }  // not sure whether 'val' should be here
record units2 { param meters: int; param seconds: int; val: real; }
Reflection.combineUnits(unit1(1,1), units2(-1, -1)) --> aNewType(param kilos=1, param seconds=-1)

Aside: perhaps obviously, we want to support measurements of integral types, not just reals. I assume this will done by adding a type parameter to unit types, which can be instantiated with real, uint(8), etc.