Closed mpusz closed 1 year ago
Is it better to be on the safe sound and disallow non-trivial conversions? Or maybe it is better to be user-friendly and disallow only what we are sure that should not be interconvertible?
@burnpanck, @JohelEGP, @chiphogg
Do we have any ideas on how to resolve that?
The only thing that somehow resonates with me here is to allow implicit interconvertibility only if we are sure about the result. This covers the following cases:
length / time
should be interconvertible with speed
)In case when some combinations result in a lack of interconvertibility (but there is at least one valid combination) a quantity_cast
should be used to make an explicit conversion if needed.
Otherwise, the quantities should not be interconvertible.
What do you think about that?
only if we are sure about the result
- we have only one interconvertible quantity of a specific dimension in the equation
(but there is at least one valid combination)
Is that possible without using friend
?
I hope so :-) I am going to compare quantity_spec
and not quantity
types. I am thinking about something like that:
namespace detail {
enum class interconvertible_result { no, yes, maybe };
template<typename... Q1, typename... Q2>
interconvertible_result are_ingredients_interconvertible(derived_quantity_spec<Q1...> q1,
derived_quantity_spec<Q2...> q2)
{
// order ingredients by their complexity (number of base quantities involved in the definition)
// if first ingredients are of different complexity extract the most complex one
// repeat above until first ingredients will have the same complexity and dimension
// check interconvertibility of quantities with the same dimension and continue to the nex one if successful
// repeat until the end of the list or not interconvertible quantities are found
}
template<QuantitySpec Q1, QuantitySpec Q2>
[[nodiscard]] consteval interconvertible_result are_interconvertible(Q1 q1, Q2 q2)
{
using enum interconvertible_result;
if constexpr (Q1::dimension != Q2::dimension)
return no;
else if constexpr (have_common_base(q1, q2))
return (std::derived_from<Q1, Q2> || std::derived_from<Q2, Q1>) ? yes : no;
else if constexpr (DerivedQuantitySpec<Q1> && DerivedQuantitySpec<Q2>)
return are_ingredients_interconvertible(q1, q2);
else
return no;
}
} // namespace detail
template<QuantitySpec Q1, QuantitySpec Q2>
[[nodiscard]] consteval bool interconvertible(Q1 q1, Q2 q2)
{
return are_are_interconvertible(q1, q2) == interconvertible_result::yes;
}
If are_interconvertible
returns maybe
, in such case quantities are not implicitly interconvertible, but the explicit conversion can be forced with quantity_cast
. I hope that will work as expected, but still, I am not sure how to deal with a list of consecutive quantities of the same dimension but different types/kinds. That is why I started this thread, and I hope that https://github.com/mpusz/units/issues/427#issuecomment-1407367145 might be good enough. Unless you have some better proposals?
Maybe I'm misunderstanding something. Let's consider the given example:
- we have only one interconvertible quantity of a specific dimension in the equation (i.e.
length / time
should be interconvertible withspeed
)
A user can add a new quantity of dimension equal to those of length / time
and speed
. So a pure function can't answer whether "we have only one" by looking only at its inputs, when the set of such quantities is extensible. Would you be able to do it using virtual
?
By "we have only one interconvertible quantity of a specific dimension in the equation" I mean there is only one ingredient quantity of each dimension in Q1...
above, and we have to check if it is interconvertible with the ingredient quantity of the same dimension in Q2...
. Q1...
and Q2...
are the ingredients used in the quantity equation used to define two derived quantities I am comparing for interconvertibility.
So to make this rule happy two quantities of dimension length and two quantities of dimension time should be interconvertible. So length / time
will be interconvertible with width / duration
, but height / time
should not be interconvertible with width / time
. I think this is quite straightforward to reason about and easy to implement. The problems start when there is more than one ingredient quantity of the same dimension in the equation (i.e. width * length / time
and length * height / time
).
So a pure function can't answer whether "we have only one" by looking only at its inputs, when the set of such quantities is extensible.
I do not care if there are any other derived quantities of the same dimension besides the two I am comparing now. In fact. there might be an infinite number of quantities of the same dimension.
I still believe that the hierarchical model I presented previously should be able to give a clear and consistent answer to all of these questions. One important thing to note is that my hierarchical model does not speak of "interconvertibility", because that word implies a symmetric relationship between the two involved quantities. I don't think that the conversion rules should necessarily be symmetric. If two quantities really are interconvertible, then it means you can substitute one for the other. That is an equivalence relation. But as such, it should be transitive: If you can substitute A for B in every place and you can substitute B for C everywhere, then you should be able to substitute A for C. Thus, I believe it is impossible to make both height
and length
"interconvertible", and simultaneously width
and length
"interconvertible", but not width
and height
. Either they are all "interconvertible", or at least one of them is not "interconvertible" to any of the other.
Given the constraint that we want to make width
and height
incompatible with each other, but each of them individually to be compatible in some suitable sense with length
, I see only one solution: That compatibility necessarily has to be an asymmetric relation. That is, between width
and length
, we want one to be able to stand in for the other, but not the other way around. In principle we can choose either direction, but the only choice that lets us have width
and height
incompatible for algebra is when length
can stand in for width
, but not the other way around. They still remain "compatible" for algebraic purposes, as I will try to highlight below:
Let us have a look at a quantity expression length + width
:
width
can be converted to length
but not vice versa, then the only consistent interpretation of the expression above is that the width
first gets converted to a length
, and then two lengths
are added, creating a length
as a result. With that (width + length) + height
also is a length, and consequently we can reorder the terms such that the subterm width + height
is valid. width
and height
are still not "interconvertible", but algebraically "compatible".length
can be converted to width
but not the other way around, then the only consistent interpretation leads to a width
result, and thus (width + length) + height
reduces to width + height
, which can remain incompatible, as there is no allowed conversion that brings both arguments to the same quantity. length
and width
were interconvertible/equivalent, we can first convert either to the other, and then perform the addition. Depending on which one we convert first, the result will be a length
or a width
, but since they are equivalent, it doesn't matter and the distinction is really meaningless. This is a viable choice, but then again, we necessarily have that width
and height
are equivalent and interconvertible too.So long story short: "interconvertibility" is not the right concept that allows us model the type of "compatibility" that we are looking for, both in function arguments and algebraic operations. We need to consider asymmetric conversion rules.
If we accept such asymmetric conversion rules, it means we could draw a directed graph of all possible quantities and their allowed (implicit) conversions. That graph would be infinite, as we need to consider all sorts of composed quantities too. Due to the associativity of quantity calculus, the graph further needs to be transitive: if A is convertible to B and B is convertible to C, then A should be convertible to C: The expression (B + A) + C
has a result of type C, but algebraically is the same expression as B + (A + C)
. Thus, once we find a cycle in this directed graph of quantities, we immediately know that each of the involved quantities are convertible to all of them and thus truly are interconvertible or equivalent for all purposes. We follow from this that the graph will form groups or clusters of equivalent quantities and a higher-order directed acyclic graph of cluster emerges (if there were a cycle among the clusters, the clusters were all equivalent and thus a single cluster - proof by contradiction).
So far we have only looked at addition. We also need to make sure that this meta-DAG of equivalent quantities respects the quantity algebra of multiplication. Distributivity says that if A
is convertible to B
, then A*C
should be convertible to B*C
, because (A+B) * C
is algebraically equivalent to A*C + B*C
. So this will be another constraint that limits conversion graphs we may propose without breaking quantity calculus.
Apart from those two constraints, we are free to draw this graph of convertibility such that it matches the expectations of our users, or the scientific and engineering communities in general. There is a simple way to ensure both constraints, both by design and in the implementation: With every convertibility we declare explicitly, we make sure that all other convertibilities that are needed to fulfil those constraints are allowed implicitly. Thus if we declare length
can be converted to width
(but not vice-versa), then, automatically, length * time
needs to become convertible to width * time
. I did describe a more detailed algorithm how to "build" that graph of convertibility from a few basic/atomic definitions in another issue before: https://github.com/mpusz/units/issues/405#issuecomment-1368941910. I did provide an illustration showing a subset of the resulting graph here: https://github.com/mpusz/units/issues/405#issuecomment-1371257667.
Now finally: Let us explore some potential choices for this graph and what it implies for the examples given in the initial issue description.
Every quantity is equivalent/interconvertible with every other quantity of the same dimension. This is what most units libraries do. But it makes width + height
acceptable, and so will it make frequency + radioactivity
(now my favourite examples of incompatible kinds because it is explicitly in the standard).
We draw some clusters of equivalent quantities following the constraints given before, but do not allow any conversion anywhere between non-equivalent quantities. This is essentially the same as dimensional analysis, except that we effectively introduce additional dimensions. For the width + height
example, we could make all of length
, width
and height
into a separate, non-equivalent group of quantities. None of them is compatible with any of the other. Crucially, we could define area
as either as a) length^2
or as b) width * height
, but the quantities length^2
and width*height
necessarily would remain non-equivalent and thus incompatible in this design. Only one of them can be the area
. Note that to "define" area
could be interpreted as defining a new quantity area
that is equivalent/interconvertible with, say, length^2
, or it could simply be an alias. Because interconvertibility is an equivalence relation, it is of no further consequence, as the two should remain substitutable in all sitauations anyway. We can exploit this in the implementation so as to make area
a strong type which is a separate but essentially equivalent quantity to length^2
.
Instead, we may want to hand-craft some of the convertibilities, and see where it leads us. To explore the examples of the initial issue description, let me just assume the following set of hand-crafted basic conversions:
length
implicitly converts to width
, but not vice versa: They are compatible, but not interconvertible. length
implicitly converts to height
, but not vice versa.height
implicitly converts to altitude
. (As a consequence, length
also implicitly converts to altitude
).length
implicitly converts to distance
, but not vice versa.path_length
implicitly converts to distance
, and vice versa: they are interconvertible/equivalent. (As a consequence, length
implicitly converts to path_length
).area
is equivalent to length^2
, they are interconvertible/equivalent.Consistency with quantity calculus leads to the following results for the examples provided in the first post:
length * length
and area
are interconvertible/equivalent by definition.area
is convertible to width * height
, but not vice-versa: The expression length * length + width * height
has a result of quantity type width * height
. Converting that to area
loses information and thus should not be allowed without explicit input from the user (quantity cast).path_length * distance
and path_length^2
are equivalent/interconvertible as required by distributivity path_length * (path_length + distance) = path_length^2 + path_length * distance
, and the term in brackets on the left-hand side are equivalent by definition.width * distance
and path_length * width
are necessarily equivalent by the symmetry of *
and distributivity again (and as expected for dimensional analysis; there is no "order" just the power of the dimension matters).path_length * height
is convertible to altitude * distance
, but not vice-versa: altitude
is "more refined" than height
.width * length
and length * height
are incompatible / neither is convertible to the other. There is no allowed conversion to any of the "parts" that leads to a common quantity type.length * distance
is convertible to path_length * altitude
, but not vice-versa: altitude
is more refined than length
(transitively through height
).To me, all of these results look reasonable. What do you think? I might be able to sketch an implementation of these rules at the start of the next week.
Crucially, we could define
area
as either as a)length^2
or as b)width * height
, but the quantitieslength^2
andwidth*height
necessarily would remain non-equivalent and thus incompatible in this design. Only one of them can be thearea
.
An area has dimension L². Both $l l$ and $w h$ (where $l$, $w$, and $h$ are quantities of length, width, and height, respectively) are valid expressions of area. It'd be nice if the library didn't require either to be more verbose just due to the way it defines area
.
All those implicit conversions in "non-trivial choices" seem backwards and unsafe. A width is always a length, and so the library could make width implicitly convertible to length. But a length doesn't necessarily represent a width, so a library that allows implicitly converting a length to a width opens up opportunities for logical errors that could be caught by the type system.
It'd be nice if the library didn't require either to be more verbose just due to the way it defines area.
I completely agree. I don't want to implement the trivial choice 2. I was trying to prove that we need a non-trivial choice if we want both "L x L" and "W x H" to be compatible with "A" (even if not interconvertible!) in the same way that "L" and "W" should be compatible, but at the same time we want "W" and "H" to be incompatible.
All those implicit conversions in "non-trivial choices" seem backwards and unsafe. A width is always a length, and so the library could make width implicitly convertible to length. But a length doesn't necessarily represent a width, so a library that allows implicitly converting a length to a width opens up opportunities for logical errors that could be caught by the type system.
I feel you, that was my first thought too, and in the V2 thread I had initially proposed rules to be the other way around. However, I believe that is a reflex coming from object oriented programming, where refining an object adds instance state. In that case, the state space actually becomes "wider" in some sense. However, refining a quantity adds a compile-time constraint to the physical phenomenon that is described by the quantity; it does not in fact change the state-space, but narrows down the space of applicability of the quantity. I believe that we have here a case of a contravariant relationship between quantities and expressions involving them. The C++ type hierarchy unfortunately does not implement contravariance even though there are many situations where that would be needed as Arthur O’Dwyer points out in the article I linked. So indeed, if we were to make actual instances of quantities to form an inheritance tree according to these refinement rules, then at least for pointers to quantities, C++ would allow us to provide a width
for a length
. But we are not talking about quantity instances here, but a hierarchy of quantity types/kinds. We can in fact implement contravariance easily in modern C++ if we do not make the quantity instances form a corresponding class hierarchy.
Let me now discuss why I think that contravariance is in fact intuitively correct for the quantity hierarchy. Consider an expression width + "1 meter"
. I believe everyone would intuitively agree that the result of this operation is still a width
, despite "1 meter" not being a width
explicitly. This can be formalised as follows: Replacing a general quantity with a refined quantity in an expression is like adding in a constraint. An expression involving multiple constraints can only be valid if all constraints are fulfilled. Thus, converting from refined to general actually removes a constraint, that is, removing information about the quantity. Therefore, converting from width
to length
is in fact unsafe, as it removes an explicit statement about the applicability of the quantity to a physical phenomenon (and therefore the applicability to algebraic expressions modelling that phenomenon).
That the converse does not lead to the expected result can be illustrated with the width + length + height
example. If we do not want to redefine the rules of algebra, then that expression should be equivalent to (width + length) + height
. If we discard the width
constraint and say it is just a length
, then ultimately, we have length + height
, which again should become a length. But the rules of algebra also say that the same expression could also be evaluated as (width + height) + length
. This is exactly the type of expression you would want to forbid, because width
and height
each have a constraint on applicable physical phenomenons that are mutually disjunct such that the combined expression is not applicable to any physical phenomenon at all.
@burnpanck Thanks a lot for your answers.
Yeah, we went a long way back and forth with the design options here. I agree with you on most points, and actually, I was the one that said that in order for 1 Hz + 1/s + 1 Bq
to not compile, we need to make 1 Hz + 1/s
return a frequency. Following this logic, width + length + heigth
should not compile as well, and the problem would be solved.
However, ISO 80000 explicitly states in multiple places:
The quantities diameter, circumference, and wavelength are generally considered to be quantities of the same kind, namely, of the kind of quantity called length.
Quantities may be grouped together into categories of quantities that are mutually comparable. Diameters, distances, heights, wavelengths and so on would constitute such a category, generally called length. Mutually comparable quantities are called quantities of the same kind.
Quantities that are comparable are said to be of the same kind and are instances of the same general quantity. Hence, diameters and heights are quantities of the same kind, being instances of the general quantity length.
the diameter of a cylindrical rod can be compared to the height of a block
However, it also states:
Quantities of the same kind within a given system of quantities have the same quantity dimension. However, quantities of the same dimension are not necessarily of the same kind.
The quantities moment of force and energy are, by convention, not regarded as being of the same kind, although they have the same dimension. Similarly for heat capacity and entropy, as well as for number of entities, relative permeability, and mass fraction.
However, in some cases special measurement unit names are restricted to be used with quantities of specific kind only. For example, the measurement unit ‘second to the power minus one’ (1/s) is called hertz (Hz) when used for frequencies and becquerel (Bq) when used for activities of radionuclides. As another example, the joule (J) is used as a unit of energy, but never as a unit of moment of force, i.e. the newton metre (N · m).
Two or more quantities cannot be added or subtracted unless they belong to the same category of mutually comparable quantities. Hence, quantities on each side of an equal sign in an equation must also be of the same kind
Of course, we can ignore the above requirements in ISO 80000 and do mathematical and strong-typing correct things. But I am not sure if that is a good idea. I hoped that we could somehow invent a way to do comparisons and arithmetics in terms of kinds and convertibility in terms of their quantity hierarchy.
Please note that function templates (i.e. op+()
) will not do automatic implicit conversions between arguments to make the code compile. So even if we make width and height interconvertible with length, they will not do that conversion by themselves.
The interconvertibility requirement comes from the usability fact that I believe that the following should work fine:
void foo(quantity<isq::length[m]>);
void boo(quantity<isq::height[m]>);
foo(isq::height(42 * m));
boo(42 * m);
otherwise, the users may complain the library is not user-friendly. However, I feel that it should not be allowed to call boo(isq::width(42 * m))
to prevent errors in interfaces.
With the rules you described, it seems that foo(isq::height(42 * m));
should not compile without an explicit cast, but this might be the most common case here. Most generic libraries will define functions like:
quantity_of<isq::speed> auto avg_speed(quantity_of<isq::length> auto, quantity_of<isq::time> auto);
If the user works on more specific types in his/her code, then it will need to explicitly cast them to use such generic interfaces. I think this is what @JohelEGP was worried about above.
But as such, it should be transitive: If you can substitute A for B in every place and you can substitute B for C everywhere, then you should be able to substitute A for C. Thus, I believe it is impossible to make both height and length "interconvertible", and simultaneously width and length "interconvertible", but not width and height. Either they are all "interconvertible", or at least one of them is not "interconvertible" to any of the other.
Not necessary. Even in the C++ language, in most cases, we support only one implicit conversion step at a time. For example, if class X
is implicitly convertible from std::string
, class X will not be implicitly convertible from const char *
as this requires two steps at a time. So a conversion between A
and C
is possible but not implicitly in one step. A user will have to explicitly provide an intermediate step of B, which will be good for code readability and maintenance.
We need to consider asymmetric conversion rules.
Sure, I am open to it. However, please keep the above foo()
and boo()
examples in mind. Which one do you think should compile, and which one not?
and so will it make frequency + radioactivity (now my favourite examples of incompatible kinds because it is explicitly in the standard).
This would not work because even though they have the same dimension and are part of the same hierarchy tree, they are different kinds, and we can easily define them like that in the library.
path_length implicitly converts to distance, and vice versa:
I am not sure why they should be interconvertible as distance
derives from path_length
the same as path_length
derives from length
.
area is equivalent to length^2, they are interconvertible/equivalent.
Does it mean that energy and moment of force are equivalent as well?
void boo(quantity<isq::height[m]>);
boo(42 * m);
This is what I'm worried about. Conversion from length to height should be explicit
.
But this is what we wanted to achieve:
std::array<quantity<isq::height[m]>, 3> a = { 1 * m, 2 * m, 3 * m };
auto h = isq::height(42 * m);
If the conversion is explicit, the first one will not compile. I am not sure about the second one.
I agree with @mpusz here: I think the boo
example should compile, and even you @JohelEGP requested this at some point in the V2 review (state-space controller initialisation). As for the foo
example, I'm not sure we would want to let that one compile.
And what do you think about the ISO 80000 requirement of comparing and adding/subtracting quantities? Should we allow that or do exactly the opposite, which was suggested above? I have implemented the ISO's behavior so far but still struggle with what to do with conversions. I think we have the following options here:
Comparison and arithmetics:
width + height
but not allow frequency + radioactivity
as those will be defined as different kinds.width + height
and frequency + radioactivity
do not compile and we do not have to play with kinds at all to achieve that.Conversions:
It seems that options 1. and 5. are the ISO's preferred approach, but that does not provide any type-safety. With that, I would prefer to abandon quantity types and quantity kinds at all, and just use dimensions. The only problem here is what @JohelEGP mentioned some time ago that there is no such thing as a speed dimension, so naming things could be harder. Such behavior would be really easy to implement, document, and standardize.
I tried hard to achieve 1. and 3. as a compromise. However, those are inconsistent in mathematical ways, as pointed out above. I am not sure if we should explore this path more?
The most C++ type-safety provides 2. and 4. which is suggested above by @burnpanck. It is consistent as well but not compatible with ISO 80000.
Yeah, you're right. Does the implicit conversion generalizes to kinds (such that a path length implicitly converts to a distance, which is a logic error if the path length is not a distance, i.e. the shortest path length between two points in a metric space) or is it limited somehow?
- Implicit downcasting (length -> width) across the same quantity branch
- Implicit upcasting (width -> length) across the same quantity branch
It seems that options 1. and 5. are the ISO's preferred approach, but that does not provide any type-safety.
The most C++ type-safety provides 2. and 4. which is suggested above by @burnpanck.
It seems to me you have the type-safety backwards. 4 is not type safe because a length isn't always a width, but 5 is type safe because a width is always a length.
It seems to me you have the type-safety backwards. 4 is not type safe because a length isn't always a width, but 5 is type safe because a width is always a length.
Agree. I maybe phrased my thoughts in a wrong way.
This is why I actually claim that we should provide interconvertibility. One is for usability and the other one is for type safety. And I hope we do not have to extend that to arithmetics and comparison so we can implement ISO's behavior. See TL;DR chapter of https://github.com/mpusz/units/discussions/426.
Ok, I understand now better what you are trying to achieve. My proposal was geared at clarity and consistency, by using the same set of rules for conversion and arithmetics/comparison. I also applied those same set of rules both to frequency
vs. radioactivity
(which ISO explicitly states as separate kinds), as well as for width
vs. height
, which are both of the same kind in ISO. My approach distinguished the latter case from the former only through hierarchy, i.e. by making width
and height
some sort of sub-kinds which still are both length
but not compatible with each other.
I believe clarity and consistency are very valuable, and we should be careful about introducing more concepts, because it makes it even more difficult for us to just even define the behaviour, let alone verify it and communicate it to the users.
Let me think a little more if I can formalise the rules that you have been suggesting under the assumption that conversion may have different rules than compatibility for algebra and comparison, so that we can make width
and height
the same kind (i.e. equivalent for comparison and algebra purposes), but not equivalent for conversion.
My proposal was geared at clarity and consistency, by using the same set of rules for conversion and arithmetics/comparison.
I love it. I would be for it if only we could make it consistent with ISO and also type-safe. However, the more experience we get, the more it looks like we need some compromise here or be not consistent with ISO 80000 rules.
Let me think a little more if I can formalise the rules that you have been suggesting under the assumption that conversion may have different rules than compatibility for algebra and comparison, so that we can make width and height the same kind (i.e. equivalent for comparison and algebra purposes), but not equivalent for conversion.
Thanks! I tried to state such rules in https://github.com/mpusz/units/discussions/426, but maybe we can find something even better.
Ok, let me recap/restate what we believe to be true and the goal that we want to achieve.
Assumptions:
A > B
<-> A - B > 0
, I believe that compatibility for addition should be exactly the same. We will have to define the result of such an operation.For comparison/addition:
frequency + radioactivity
should be disallowed.width
and height
are such examples, then width + height
should be allowed. The only reasonable result of that operation would be a general length
then.For conversion:
width
and height
are compatible for comparison (in this hypothetical "ideal" design), but they should never convert implicitly, because they are on separate branches of the "quantity type tree" within a single "kind".length
to width
): @mpusz and @burnpanck are in favour of allowing that, @JohelEGP is against (correct, or did I misunderstand?).width
to length
): @mpusz wants to allow that, I'm undecided yet. @JohelEGP seems to be in favour, as this is the usual "covariant" compatibility implied by a C++ type hierarchy ("type safety" - though I'm not sure that this term has any meaning here on it's own, nor that we should necessarily represent the "quantity type hierarchy" as a hierarchy of quantity<...>
types in C++. IMHO we should rather try to understand what conversion behaviour best models physical quantities.)Do we all agree on that?
I agree with all of the above :-)
We want to model ISO kinds exactly
Probably it would be good to do so unless we decide to ignore ISO 80000. One thing to note here is that ISO does not specify which quantities are of different kinds. It just provides a few examples and claims that the division into kinds is to some extent arbitrary.
"type safety" - though I'm not sure that this term has any meaning here
By "type safety" I mean that we should not be allowed to call void foo(quantity<isq::width> w, quantity<isq::height> h);
with reversed order of arguments. This in my opinion is the best we can do in those circumstances but, of course, I am open to some better proposals.
- "implicit narrowing" (
length
towidth
)
I wouldn't call that narrowing. It's implicitly adding semantics to a value by type conversion, from a more generic type to a more specific type.
Also, please clarify what your feeling towards path_length
and distance
is. I assume they are of the general kind of "length", so comparison to width
and height
should be allowed. So among those four, are they all on separate branches , or are path_length
and distance
"closer" in some way. (Ideally, draw me the hierarchy).
This is my understanding
length
/ | \
width height path_length
|
distance
at least this is how I implemented it here: https://github.com/mpusz/units/blob/v2_framework/src/systems/isq/include/mp_units/systems/isq/space_and_time.h.
"implicit narrowing" (length to width)
I wouldn't call that narrowing. It's implicitly adding semantics to a value by type conversion, from a more generic type to a more specific type.
Ok, then let's not call it narrowing. @JohelEGP Please confirm what type of "vertical" conversions you would like to allow, and which you don't. My understanding is you want to allow width
to length
but not the other way around?
I agree with all of the above :-)
We want to model ISO kinds exactly
I agree, too.
or are
path_length
anddistance
"closer" in some way.
See https://github.com/mpusz/units/issues/405#issuecomment-1339621239.
My understanding is you want to allow
width
tolength
but not the other way around?
Only for conversions.
Length to path length, path length to distance, and length to distance. All those would treat the source value as valid for the target type. You could make arguments for length, which is used to construct more specific quantities of length. But a path length doesn't necessarily represent a distance. So permitting such a conversion implicitly, when it can be avoided in the type system, seems like a bad design choice.
But a path length doesn't necessarily represent a distance
I think this exactly the same case as "length doesn't necessarily represent a width" but I assume you would like to be able to write:
quantity<isq::path_length> pl = 42 * m;
or call:
void foo(quantity<isq::path_length>);
with 42 * m
without any additional casts, or do isq::path_length(42 * m)
. The question here is if it should be possible to call it with isq::distance(42 * m)
, but I think that for the sake of consistency, it should. The reason why is that I think that all of the 42 * m
, isq::length(42 * m)
, and 42 * isq::length[m]
should mean exactly the same. However, they will result in quantities of different types. The first case will return quantity<si::metre, int>
while the other two will return quantity<reference<isq::length, si::metre>, int>
. So, in theory, we could create a rule that quantity can be downcasted only if it is specified in terms of a unit and not a reference
type. Giving it a specific reference could mean something more than just a unit, even if the quantity_spec
is exactly the same in both cases.
Following on the above:
quantity<si::metre>
being a result of 42 * m
could mean quantity of a length kind and be downcastable to any quantity type of that kind.quantity<isq::length[si::metre]>
being a result of 42 * isq::length[m]
or isq::length(42 * m)
can mean a concrete quantity type in a hierarchy and could be not downcastable (as @JohelEGP suggests).That can actually work. So every quantity has a kind, but there are quantity that do not have a "quantity type", and there are quantities that do have a "quantity type". Rules for compatibility for comparison and algebra are only specified in terms of the kind and never take into account the "quantity type". Rules for implicit casting are as follows:
This does indeed seem to tick all the boxes. There may still be some bikeshedding around how to call that "quantity type" concept.
What we will still have to figure out however is how to define the kind of a product of two quantities, with or without type. I believe the "quantity type hierarchy" is still this infinite DAG that follows the rules of quantity calculus as discussed before, and as such I don't believe there is any problem here. My gut feeling is that we may have to put some extra work in still to make the kinds work in the case of energy and moment of force. But I'm confident we will find a solution here!
Some questions to consider:
energy + force*length
? If we don't, how do I integrate the energy stored in an elastic material given it's stress/strain curve?energy + current*voltage*time
? If we don't, how do I integrate the energy stored in a capacitor?count/time < frequency
? If we don't, then how do I produce the output of a "frequency counter"?count/time < radioactivity
? If we don't, then how do we compare the result from a geiger counter against an exposure limit?count < frequency*time
? IMHO it should be allowed exactly iff 3. is allowed.frequency < radioactivity
.frequency*length < radioactivity*length
.frequency*time < radioactivity*time
either.What are your thoughts here?
Just to provide some examples:
Quantities without a quantity type implicitly convert to a quantity of the same kind and any type (assuming the representation type also qualifies for implicit casting).
void foo(quantity<isq::width[m]> w, quantity<isq::height[m]>);
foo(42 * m, 42 * m); // OK
foo(42 * isq::width[m], isq::height(42 * m)); // OK
foo(isq::height(42 * m), 42 * isq::width[m]); // ERROR
foo(isq::length(42 * m), 42 * isq::length[m]); // ERROR
Quantities with a quantity type do only implicitly cast "upwards".
I do not think that is good. I think it would be good to allow the following to work without any explicit casts:
quantity<isq::speed[m/s]> avg_speed(quantity<isq::length[m]>, quantity<isq::time[s]>);
avg_speed(isq::distance(42 * m), isq::duration(2 * s));
What we will still have to figure out however is how to define the kind of a product of two quantities, with or without type
The way I have implemented it is that the kind of a product of two quantities is always a product of their kinds. So both length / time
and distance / duration
will create a quantity of length / time
kind. This works already, and I believe is correct.
The more interesting question is what should be a kind of 42 * N
. Should it be mass * length / time^2
, or should it be force
? In the latter case, we will have to provide a quantity kind for every named SI unit, which is fine by me as we wanted to do it for Hz, Bq, and J anyway. Extending this requirement to all named derived units in the SI system seems reasonable,
Do we want to allow energy + force*length? If we don't, how do I integrate the energy stored in an elastic material given it's stress/strain curve?
Yes, this is tricky. As force * length
will be of a different kind (but the same dimension) than energy according to ISO 80000 it should not compile. To make it compile user will have to do energy + isq::energy(force * length)
.
An alternative approach could be the one I started at https://github.com/mpusz/units/issues/429. We could assume that kinds also form a hierarchy, and then energy + force*length
will produce a quantity that is of force * length
kind. Such a quantity could be converted to energy again. Implicit conversion would be allowed for 42 * J + 42 * N * (2 * m)
and explicit if we write isq::energy(42 * J) + isq::force(42 * N) * isq::height(2 * m)
.
The same answer is for 2.
Which one do you prefer?
3-5 are tricky indeed. ISO claims those should be not comparable if we define them as being of different kinds. Although users may be confused by the fact that they do not compile and the error message they get might not be the easiest one to understand.
I agree with 6-8.
Another interesting point is what should we get from isq::energy(42 * J) + 42 * N * (2 * m)
or 42 * J + 42 * N * isq::height(2 * m)
. I believe that in such cases the quantities should use their kinds as types and form a quantity with a specific type. Does it make sense?
isq::path_length(42 * m)
If isq::path_length
were a type, that'd be able to invoke an explicit
constructor, so it's syntax I'd expect to work.
but I assume you would like to be able to write:
quantity<isq::path_length> pl = 42 * m;
or call:
void foo(quantity<isq::path_length>);
Those would require implicit conversions. As I replied at https://github.com/mpusz/units/issues/427#issuecomment-1412417240:
You could make arguments for length, which is used to construct more specific quantities of length. But a path length doesn't necessarily represent a distance. So permitting such a conversion implicitly, when it can be avoided in the type system, seems like a bad design choice.
Replying to https://github.com/mpusz/units/issues/427#issuecomment-1412551747.
Some questions to consider:
For 1-4, the answer can be "explicitly cast the lhs to the rhs' type". Otherwise, it should be a question of how much verbosity we require (because those operations should be possible) and possible safety concerns in allowing those.
The way I have implemented it is that the kind of a product of two quantities is always a product of their kinds. So both
length / time
anddistance / duration
will create a quantity oflength / time
kind. This works already, and I believe is correct.
I agree. To be extra precise, this is the behaviour I propose: Dividing a quantity of "type" distance
(which is of kind length
) by a quantity of "type" duration
(which is of kind time
) results in a quantity of "type" distance*duration^-1
, which is of kind length*time^-1
.
Yes, this is tricky. As
force * length
will be of a different kind (but the same dimension) than energy according to ISO 80000 it should not compile.
There is a loophole that we could invoke: We may argue that multiplying a force
by a length
is not exactly the same as specifying a quantity in "newtonmetre". Thus, while the latter clearly is a quantity of ISO 80000 kind moment_of_force
, there is some room of interpretation what the former product actually is. One interpretation could be exactly as you say:
An alternative approach could be the one I started at https://github.com/mpusz/units/issues/429. We could assume that kinds also form a hierarchy
This allows us to declare compatibility rules for summation of non-equivalent kinds of quantities, making use of that hierarchy. Then the product of a force
and a length
could be a "base" kind force*length
, while both energy
and moment_of_force
are refined/narrower kinds. But as discussed earlier, to prevent moment_of_force
from being compatible with energy
due to the transitivity implied by the associativity of summation, the sum of force*length
with a energy
would have to produce a energy
, not a force*length
. So while this is not a case of conversions, the result would be the same as if we had explicitly down-casted force*length
to an energy
in the operation.
We have established that we want to decouple the conversion rules from the compatibility rules for addition, and furthermore that conversions shall be determined by the "quantity type" while compatibility for addition shall be determined by the "quantity kind" which do not necessarily need to overlap. Thus converting only upwards in the quantity type hierarchy and additions resulting in the most refined type in the quantity kind hierarchy is not necessarily a problem.
With that arrangement we can also make both frequency
and radioactivity
sub-kinds of time^-1
in the quantity kind hierarchies and we can allow all of 1-5 while still prevent 6-8. But that may be too complicated.
For 1-4, the answer can be "explicitly cast the lhs to the rhs' type". Otherwise, it should be a question of how much verbosity we require (because those operations should be possible) and possible safety concerns in allowing those.
The questions were specifically meant to be understood as "implicitly allowed" without explicit cast being required. I would rather not have to write quantity_cast
in those cases, because quantity_cast
is a vastly too powerful beast for this purpose (you know my feeling towards the all-powerful quantity_cast
by now ;-D). But I agree that isq::energy(force*time)
is clear here, and I'm fine with that. I have a feeling that making a separate hierarchy for kinds that behaves somewhat differently to the quantity type hierarchy may be confusing. So if we are happy that 1-5 requires an explicit cast, then we don't need a hierarchy of kinds as two kinds are either exactly the same, or they are incompatible.
additions resulting in the most refined type in the quantity kind hierarchy is not necessarily a problem
In such a case, what would be the result of the width + length + height
that ISO 80000 requires to work?
As I stated in #429, the following is really confusing:
quantity<isq::frequency, int> f = 1 * Hz + 1 * Bq + 1 * Bd;
width + length + height
is easy. All of them are of the ISO kind of length
, so it actually doesn't matter if we are implementing a hierarchy of kinds or not. As for the "quantity type", we all agree that width
and height
are both more restricted than length
. So the result is still a quantity of kind length
but also "type" length
, as this is the only logical type. It happens to be consistent with our conversion rules for the "quantity type* too!
you know my feeling towards the all-powerful quantity_cast by now ;-D
I am not sure if you have seen 5.2.1 in https://github.com/mpusz/units/discussions/426? 😉 With that quantity_cast
could be left only to explicitly cast quantity types and kinds.
Sorry, didn't have time to look at #426 yet, I'm still supposed to do some work for my company :-D.
As for the 1 * Hz + 1 * Bq
case: Our "tick-all-the-boxes approach" says that this is the sum of two quantities that have no type, but of incompatible kinds frequency
and radioactivity
. Thus the expression should be disallowed, and there is no need to define either a resulting kind or type. The same applies to frequency + radioactivity
. While both quantities do have a "type" now, they are still incomaptible kinds.
Ahh OK. I thought that by the "additions resulting in the most refined type in the quantity kind hierarchy is not necessarily a problem" you meant that we should find a common the most refined base kind for incompatible kinds.
Things are different if we look at (1/(1*s)) + 1*Hz
, 1/time + frequency
and 1/(1*s) + frequency
though. Let us first discuss the "quantity type" assuming the expressions were allowed, because that one is easier. The first expression is a sum of two quantities with no type and by assumption compatible kind, so the result still has no type, and we'll discuss the kind later. The second is the sum of a quantity of "type" time^-1
with a quantity of "type" frequency
. Assuming frequency
is a more refined "type" than time^-1
, then the result is of the more general "type" time^-1
, consistent with the width + height
-> length
example and our conversion rules that we defined for the "type". The third example finally would be the sum of a quantity with no type with a quantity of type frequency
, for which I propose the result to be a quantity of "type" frequency
(consistent with our conversion rule exception that allows no-type quantities to down-cast to any refined type).
So if we want to declare frequency
and 1/time
compatible for addition, I just presented consistent rules for the "quantity type". The challenge however is the rule for the kind which tells that these are in fact compatible, while still keeping frequency
and radioactivity
incompatible. We know we cannot make 1/time
compatible with frequency
by making them the same kind, because we should then equally do the same between 1/time
and radioactivity
, and then frequency
and radioactivity
would be transitively compatible too. So we have to make 1/time
and frequency
different kinds that are still compatible. This either contradicts ISO 80000 right away, unless we call up the loophole that we define a hierarchy of kinds which ISO 80000 doesn't mention. Then, we can achieve our desired behaviour by making the resulting kind of 1/time + frequency
be frequency
(even though we have established that the resulting "type" is time^-1
). But, as you see that this can quickly become confusing, as we now have a general "type" for a refined kind, and I haven't thought it through if that is really going to work out. We may be better off to just make frequency
and 1/time
incompatible.
Yeah, I have been asking similar questions to myself for a few weeks now. Thanks a lot for your help! Hopefully, together we will be able to find something reasonable.
I just added a new discussion thread (#426) about the V2 design rules. As stated in TL;DR, the main idea is to:
foo(length, width)
. It turns out that this point is quite hard to define for derived quantities.Should the following quantities be interconvertible?
length * length
andarea
width * height
andarea
width
andheight
are interconvertible withlength
so this probably should be true as wellpath_length * distance
andpow<2>(path_length)
width * distance
andpath_length * width
distance
is interconvertible withpath_length
and not withwidth
altitude * distance
andpath_length * height
altitude
is interconvertible withheight
, anddistance
is interconvertible withpath_length
width * length
andlength * height
width
andheight
are interconvertible withlength
width
is not interconvertible withheight
length * distance
andpath_length * altitude
distance
is interconvertible withpath_length
andlength
is interconvertible withaltitude
path_length
is interconvertible withlength
and thendistance
is NOT interconvertible withaltitude