mpusz / mp-units

The quantities and units library for C++
https://mpusz.github.io/mp-units/
MIT License
997 stars 79 forks source link

`polymorphic_unit`??? #483

Open mpusz opened 10 months ago

mpusz commented 10 months ago

Sometimes, people want to deal with quantities of a specific type but with a unit unknown at compile-time. For example, a specific unit might be obtained from some configuration file or from the web by some means. Right now we are forced to use std::variant but it is far from being easy and user-friendly to use.

Maybe something like a polymorphic_unit should be considered?

JohelEGP commented 10 months ago

Sometimes, people want to deal with quantities of a specific type but with a unit unknown at compile-time.

I know of at least two use cases that warrant different solutions.

For the former, I think std::format actually needs a value like scaled(x, si::ratios), so the algorithm internal to the formatter know which scales to use.

For the latter, we can support it by making reference a non-static data member. But the current implementation of reference uses magnitude, which isn't runtime friendly. So much more massaging is necessary for this approach to work.

mpusz commented 10 months ago

I would say that the former is out of scope for this issue. It can be addressed by providing a specific token in a grammar, and the implementation would be similar to the capacitor_time_curve example.

chiphogg commented 10 months ago

I wanted to note my comments from our last meeting here, for posterity.

  1. This is probably a good idea. I believe there are use cases for this. One example is speed limits, which are usually integers, but which can be represented in MPH or KPH in different contexts. Users can get everything they need from variants, but using variants directly can be cumbersome, so there's value in providing a more usable interface.

  2. We probably shouldn't do it. This is purely for reasons of opportunity cost. We already have more work than we can do in the time we have. We're better off waiting for an actual, concrete use case to arise --- probably via an issue filed by a user with a specific problem to solve. Note too that the nature of this feature is that it's very easy to add on after the fact. We won't regret standardizing the rest of the library without this feature, but we easily could regret standardizing a suboptimal implementation of this feature.

Therefore, I suggest we close this issue and reduce our list of open design issues.

mpusz commented 10 months ago

We probably shouldn't do it.

Of course, I do not claim we should implement it now, but at least consider if we find it useful and discuss the implementation cost.

Note too that the nature of this feature is that it's very easy to add on after the fact.

I am not so sure it is that easy to add later. As a concrete unit will be only known at runtime, we will need at least a few important changes:

Does anyone see any other important points that require change with this feature?

JohelEGP commented 10 months ago

Does anyone see any other important points that require change with this feature?

We don't have types to represent non-integral amounts of an unit.

chiphogg commented 10 months ago

I assumed we would build the feature non-intrusively, on top of quantity. But if we did that, I guess it would be more like "polymorphic quantity" than "polymorphic unit".

It's true that a kind of "variant of units" might require a more invasive approach. But I also don't see the value that would add over and above the "variant of quantities" approach.

mpusz commented 10 months ago

We don't have types to represent non-integral amounts of an unit.

Why would we need it? My understanding is that we will be working with proper units but only known at runtime, so the conversion will happen at runtime in sudo_cast implementation based on the amount/factor/ratio described by a specific unit pointed by the type-erased one. It is somehow similar to the runtime currency conversion factor that I implemented some time ago but as we decided to not follow this path then, the branch was removed.

JohelEGP commented 10 months ago

I suppose it's doable if the amount of unit is a double that's eagerly evaluated. Unlike mag, which keeps the roots around.

mpusz commented 9 months ago

https://www.boost.org/doc/libs/1_83_0/doc/html/boost_units/Examples.html#boost_units.Examples.RuntimeUnits

Attempt3035 commented 5 months ago

I wanted to note my comments from our last meeting here, for posterity.

  1. This is probably a good idea. I believe there are use cases for this. One example is speed limits, which are usually integers, but which can be represented in MPH or KPH in different contexts. Users can get everything they need from variants, but using variants directly can be cumbersome, so there's value in providing a more usable interface.
  2. We probably shouldn't do it. This is purely for reasons of opportunity cost. We already have more work than we can do in the time we have. We're better off waiting for an actual, concrete use case to arise --- probably via an issue filed by a user with a specific problem to solve. Note too that the nature of this feature is that it's very easy to add on after the fact. We won't regret standardizing the rest of the library without this feature, but we easily could regret standardizing a suboptimal implementation of this feature.

Therefore, I suggest we close this issue and reduce our list of open design issues.

Hey all! I've got a use case that might be relevant here, and while I'm only just getting started with this library (and may have missed an obvious way to do this), I think it's probably a likely scenario in other use cases too!

I have a scenario where I'm sending data between a simulation (one of a few variants) and an endpoint. The data has a key which refers to what it's for (ie, a car's tachometer) and it's corresponding value. I'm looking to also send the data with a unit, such as revs/m, but knowing that the unit might be different from different simulations, for the same data. I'd then need to do a runtime conversion to the unit appropriate for the endpoint.

It would be suuuuper easy to write the implementation if I can convert between two unknown types like this at runtime safely.

Secondly to all this, is there any possible means of having an enumeration or similar unique identifier for each unit type? As in, I'm not sure of a way the data type can be represented / serialised and cast back to the safe, correct type at the other end. My thoughts at the moment is that it would be great if there was a reliable mapping (maybe that is also versioned and values aren't reused so old and new versions can still interface nicely) of types to enumerations (which a getter function with a polymorphic return would work well with) so that data can be encoded, sent, decoded and operated on while keeping all the great aspects of using this library's classes.

Thoughts?

chiphogg commented 5 months ago

Hi @Attempt3035! Here are a few thoughts.

First: if it's natural for your simulation variants to know the unit that the endpoint requires, the easiest solution would be to convert the values into that unit before serializing them. You could indicate that unit via a suffix on the name of the serialized variable: for example, if the endpoint wants a speed in m/s, you could name the serialized field speed_mps.

As for a serializable enum: I think that's going to depend on the set of units that are relevant for an individual use case, which is not something we could know in a general way. For example, you could create a serializable enum with values for m/s, MPH, KPH, and whatever other speed units you need. And you could serialize a quantity of any of these units by serializing its value in that unit, along with the corresponding enum value.

I don't anticipate being able to support an unbounded set of units in this way, though. It would raise too many questions in my mind; I worry about excessive complexity. What information about the unit would we encode? Its name only, or also its symbol, dimension, magnitude, quantity kind, ...? What if two different programs define two different units with the same name? And on the decoding end: if we encounter a quantity that was serialized with a unit we don't have access to, what should we do?

For data on the far side of the "serialization boundary", experience has taught me to prefer representations that are both simple and unambiguous. Examples include a single fixed unit indicated by a suffix on the field name, or a value-plus-enum where the latter can indicate one of a predetermined set of unit options.

Attempt3035 commented 5 months ago

Hey @chiphogg! Those are some really good points you make!

For my system architecture, we try to keep the simulations decoupled and considering the simulation plugins can be in different languages, we don't want to rewrite conversion code for each to cater for every case the endpoint could ask for. In this way, we have the c++ engine in the middle that bridges everything and performs the safe conversions, hence the need to serialize and send the unit types.

Yes, defining a project specific enum is probably appropriate. You raise great points about what it would or wouldn't include, there's likely too many possible combinations for a universal solution to make sense. I guess my initial thought was that it would act sort of like a capnproto message that would be backwards compatible across versions, but that sort of functionality is definitely beyond the scope of the project. I guess it sort of makes it more like a platform independent data type binding system (although that would be handy😝)

Yes, I guess I'll keep it implementation specific and define an enum with just the units that get used and define the conversion function to deserialise the data into the appropriate unit type. It will be easy to do unit conversions from there on out anyway! Hmm, haven't looked into it much yet but it might be quite annoying to define the return type for that function as the list grows, I think I could use something like boost.any? But I do wonder how a polymorphic return type would work in comparison as far as improving compile time safety and minimising lengthy variants... Once I dive further into the code I'll see how it goes😝

kwikius commented 4 months ago

FWIW I found that the cases for a polymorphic unit are often limited to a few units {mph , kph } { in, cm, m } in a user interface. This can be dealt with by a simple parser and a map e.g https://github.com/kwikius/quan-trunk/blob/master/quan_matters/examples/quantity_map.cpp#L86 . Might be an interesting example to add to mp-units

Attempt3035 commented 4 months ago

That's an awesome help, thank you!