policies at the intersection of algebras, unit conversion and value conversion #237
Replies: 16 comments 101 replies
-
One possible tier, call it Tier 0: no operators are applied unless both value-type AND unit-type are identical. If you need to add two values and they are not identical, you have to explicitly convert using This is arguably the "safest", or least ambiguous, tier - there are no contextual assumptions about whether or how to convert units or values, or in what order, etc. One could always support this tier out-of-box. |
Beta Was this translation helpful? Give feedback.
-
Tier 1 is cases where you have
So, is there an out-of-box policy here? A possible policy is to not get into this kind of ambiguity. Only support operations when left and right are identical. This is actually a very reasonable policy, and I can see having it be one that the user can import. Another possible policy is to involve a new kind of typeclass, a ValueResolver, that maps (at type level) (VL, VR) -> VO. For example, a standard resolution in built-in types is (Double, Int) -> Double, or (Int, Long) -> Long, etc. The standard numeric promotions. However, things get more ambiguous for something like spire types. There is no standard consensus on whether Rational is ">=" Algebraic or not, and spire does not provide one. I could perhaps define a few "obvious" ones, or simply say that if someone wants to use this kind of policy, they have to supply their own ValueResolver. One could define the obvious standard ones, like ValueResolver[Double, Int] -> Double, etc. this kind of policy also raises the issue of which "value space" to do the actual operation in. I believe I have an idea here, which is to always do the operation in the output-type space VO. This policy would be a straightforward generalization of the "big-4" builtin types. For example, Int+ Double is basicaly: Resolve (Int,Double) -> Double (VO), and the semantic is to map Int->Double, and Double->Double (identity) and add the two doubles to get the result. If you have some non-standard types (e.g. from spire), and you do not have a coherent ValueResolver policy, then this will simply fail to compile, and you will have to do all your operations over identical types, and do any value conversions with |
Beta Was this translation helpful? Give feedback.
-
Tier 3 - One question we can ask: does one convert values, and then convert units? or does one convert units, then values? I believe that the "best" answer is to convert values, then units. Why? Assume that we are also using the ValueResolver concept in the section above. In theory almost any crazy Resolver policy is possible, however practically speaking, the semantics of ValueResolver is PROBABLY to up-cast to numeric types that have more resolution, or at least to not throw a significant amount away. Again, the standard types are instructive: we resolve (Int, Double) -> Double. And even for non-standard types a similar property will hold. One is unlikely to Resolve (Int, Double) -> Int, and throw away fractional values, to do these kind of computations. So a reasonable, and likely numerically safe, policy is:
Note, if one is multiplying or dividing, UO will not be UL, it will be a new unit type equivalent ot UL*UR or UL/UR, etc The above runs into a possible issue if both VL and VR are integer types. It's not hard to simply apply the policy above, however doing unit conversions in integer spaces can result in massive loss of precision. For example Converting 1 yard to meters, in integer space, is going to result in 0 meters. In "coulomb-2 scala", I basically just allowed this, and assumed the user could work these issues out for themselves. That is one reasonable possibility. Another possibility I'm toying with is to not, by default, support these operations if VO is an integer type, unless it is explicitly enabled.
Currently thinking (3) is cleanest, and easiest for a user to extend for custom types. |
Beta Was this translation helpful? Give feedback.
-
@cquiroz @armanbilge I have an implementation of the above ideas, which I am pretty happy with |
Beta Was this translation helpful? Give feedback.
-
@erikerlandson I hope you don't mind, I spent this evening starting from tabula rasa to explore some of my own ideas. Obviously I've spent a tiny fraction of the time you've spent thinking about this and don't really have a deep understanding of the problem space. In other words, I apologize in advance for the naivete and stupidity that follows :) The ideas I explored are extremely underdeveloped, but roughly fell into these two categories: A "tagless final" style APITagless final is the API design adopted by several modern Typelevel libs. Basically the idea is to separate libraries into:
The canonical example is Cats Effect 3: the kernel provides typeclasses Then, Cats Effect core provides the The same pattern roughly applies to algebra/spire: algebra is the kernel, with its algebraic typeclasses, and spire provides concrete numerical types. So, what does this mean for coulomb? IMHO units are an implementation detail: it should be possible to write code that operates solely in terms of the base quantities (e.g., time, length, mass) without knowing the actual units of measure. Ideally something like: def f[Q[_]](x0: Q[Length], v0: Q[Length / Time], a: Q[Length / Time^2], t: Q[Time]): Q[Length] =
x0 + v0 * t + 0.5 * a * t^2 Here IMO, this approach has some nice features:
I explore what these typeclasses could look like here although I haven't quite figured out how to make the ergonomics as nice as the ideal example above. Of course, we can't ignore units forever :) I think there are really two situations we need to be concerned with units.
A maximally type-level APIThe other idea I explored was aggressively static typing the API, taking advantage of several Scala 3 features. This moves a large chunk of the dimensional analysis logic to the type-level and thus compile-time. A type-level Abelian group for unitsFor a fixed set of dimensions (e.g., the 7 SI dimensions) it is possible to express an effectively infinite combination of base quantities completely at the type-level in a composable way. This is thanks to Scala 3 support for compile-time arithmetic. coulomb/core/src/main/scala/coulomb/SIDimension.scala Lines 23 to 31 in 1dad497 Here, we are expressing our position as integer-valued coordinates in terms of time, length, mass, electric current, temperature, amount of substance, and luminous intensity. This makes it easy to multiply units completely at the type-level, such that the dimensional analysis is happening completely at the type-level/compile time. coulomb/core/src/main/scala/coulomb/SIDimension.scala Lines 38 to 41 in 1dad497 Another bit of magic is that we can now define various aliases and the compiler will automatically be able to recognize the relationships between them. type Velocity = Length / Time Statically-typed units
coulomb/units/src/main/scala/coulomb/units/Unit.scala Lines 19 to 28 in 1dad497 There are some interesting things we can do with this as well. Suppose we define: type Inch = DimUnit[Rational[254, 1_000], Meter] One of the concerns with unit conversion for integer values is whether to support conversions that lead to loss of precision. For example, attempting to convert an integer inch to an integer meter, centimeter, or millimeter. However, it is possible to convert an integer inch to an integer micrometer or smaller without loss of precision. Because we can do arithmetic on That's all for now, thanks for reading :) |
Beta Was this translation helpful? Give feedback.
-
I'm interested in the possibility of making type-level coefficient definitions work: one weird irony of working in And yet (another irony) : I can get the actual numeric vals if they are types! So, a typelevel representation that allows me to break the I'm unsure of the total extent of the benefit: most unit expressions are not "deep" - but it would not be zero. One idea I had was to allow string literals: IIUC, we could make the coefficient type a string "12345643353262533 / 432352345235234", Or maybe support a more extensible type structure like you suggested: Part of me wants to explore this later - my brain can only handle so much at one time - on the other hand, if I'm going to really do it, sooner is probably better. |
Beta Was this translation helpful? Give feedback.
-
A separate thread for Abstract Quantities (Length, Mass, Duration) I can think of a couple possibly interesting directions for this.
Direction (1) seems more promising, especially if we can implement clean semantics for type expressions such as |
Beta Was this translation helpful? Give feedback.
-
It is possible to lift some special cases all the way up to extension[UL](ql: Quantity[Double, UL])
transparent inline def +[UR](qr: Quantity[Double, UR])(using coef: Coefficient[UR, UL]): Quantity[Double, UL] =
val c = coef.value.toDouble
print(s"c= $c\n")
(ql.value + c*qr.value).withUnit[UL]
extension[VL, UL](ql: Quantity[VL, UL])
transparent inline def +[VR, UR](qr: Quantity[VR, UR])(using add: Add[VL, UL, VR, UR]): Quantity[add.VO, add.UO] =
add(ql.value, qr.value).withUnit[add.UO] scala> 1d.withUnit[Meter] + 1d.withUnit[Kilo * Meter]
c= 1000.0
val res0: coulomb.quantity.Quantity[Double, coulomb.units.si.Meter] = 1001.0
scala> 1f.withUnit[Meter] + 1f.withUnit[Kilo * Meter]
val res1: coulomb.quantity.Quantity[Float, coulomb.units.si.Meter] = 1001.0 |
Beta Was this translation helpful? Give feedback.
This comment has been hidden.
This comment has been hidden.
-
@cquiroz @armanbilge I could in theory do this in other places, for one example the unit definitions. Another example would be My holy grail has always been the ability to import things such as unit definitions from multiple places, and have it act as a true "union" - so I could get the SI units (meter, kilogram, second, etc) from |
Beta Was this translation helpful? Give feedback.
-
I feel like these definitions of si units in the form of physical constants will make good unit tests, for the coulomb physical constants. I'm thinking of moving these into the https://en.wikipedia.org/wiki/2019_redefinition_of_the_SI_base_units#Impact_on_base_unit_definitions |
Beta Was this translation helpful? Give feedback.
-
@armanbilge @cquiroz I created a parallel version of addition: I don't have any intuition about what scala does with manifestations like this that appear in |
Beta Was this translation helpful? Give feedback.
-
@armanbilge @cquiroz it seems like the kind of thing I have in mind but it's not very active, the last update was a year ago, and the examples are all on sbt 0.13, and scala 2.11 my attempts at code diving munit to add benchmark timing outputs weren't very fruitful, it's pretty oriented around only reporting assertion errors, but it would be cool to add a microbenchmarking facility |
Beta Was this translation helpful? Give feedback.
-
@armanbilge @cquiroz as of #257 and #258 coulomb-core for scala 3 is effectively complete 🎉 Needs scaladoc, and any additional unit testing I can think of, but it implements all the functionality I planned for it |
Beta Was this translation helpful? Give feedback.
-
Ever since I started this project I have puzzled over what the "best" policies are for coulomb to support out-of-box in terms of numeric operations (algebras) as they interact with unit conversions and value conversions.
In the current ("scala-2") version of coulomb, I "hard-code" this interaction to a certain degree: The convention is:
(see here for description)
In scala-3 coulomb, I have redesigned the system of context ("implicit") types and functions so that there is no a-priori bias toward LHS value types (or units, even): both the output value type and unit type are dependent types, taking advantage of scala-3's significantly improved support for dependent typing.
For better or worse, this re-opens the question of what out-of-box policies are best to support (or even how to define what "best" is).
In this thread I'm going to try and articulate some possible "tiers" of policies, particularly when generalized to working with typelevel/algebra (AdditiveSemigroup and friends), and the numeric types in typelevel/spire.
cc @cquiroz @armanbilge
Beta Was this translation helpful? Give feedback.
All reactions