Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Poll: How to constrain operations in a class template? #29

Closed
mpusz opened this issue Nov 9, 2019 · 9 comments · May be fixed by #492
Closed

Poll: How to constrain operations in a class template? #29

mpusz opened this issue Nov 9, 2019 · 9 comments · May be fixed by #492
Labels
help wanted Extra attention is needed Polls

Comments

@mpusz
Copy link
Owner

mpusz commented Nov 9, 2019

The more I think about the "number" concept (#28) the more doubts I have. I see 2 solutions here:

  • Option 1

    template<Unit U, Number Rep = double>
    class quantity {
    public:
      template<Number Value>
      constexpr quantity& operator%=(const Value& rhs);
      // ...
    };

    In this case, Rep has to satisfy all of the requirements of the Number concepts. It means that even if the user is never going to use operator %= on a quantity his/her representation type has to provide it or the quantity class template instantiation will fail.

  • Option 2

    template<typename T>
    concept UnitRep = std::regular<T> && std::totally_ordered<T>;
    
    template<Unit U, UnitRep Rep = double>
    class quantity {
    public:
      template<Number Value>
      constexpr quantity& operator%=(const Value& rhs)
          requires requires(Rep v) { { v %= rhs } -> std::same_as<Rep&>; };
      // ...
    };

    In this case, the user will be able to instantiate the quantity class template for every "sane" type and will be able to use only those arithmetic operations that are supported by the underlying Rep type.

Which option do you prefer?


(please vote only once by clicking on the correct bar above)

@i-ky
Copy link
Contributor

i-ky commented Nov 9, 2019

@mpusz, here you have pointed out that

quantity takes Rep as a template parameter and maybe it could be some vector representation there.

Option 1 will not allow it, because division is not defined (mathematically speaking) for vectors. This is the first reason, why I would prefer Option 2.

The second reason to prefer Option 2 is that it can potentially provide a way to have non-additive quantities and stuff like that. E.g. it would make sense to prohibit adding absolute temperatures to one another using a restrictive Rep for quantity.

@mpusz
Copy link
Owner Author

mpusz commented Nov 9, 2019

@i-ky Thanks for your feedback! Regarding "second reason" we will probably solve the temperature problem with a dedicated affine space support. There will be no need to restrict Rep type for it.

@i-ky
Copy link
Contributor

i-ky commented Nov 11, 2019

I guess by "temperature problem" you mean conversions between unit that have different zeros. I had a different thing in mind.

Imagine we have two temperatures (could be air temperatures in different places or on different days). What would adding them up mean? Does the sum of two temperatures have physical meaning? I think no. However, it makes sense to multiply temperature by Bolzmann constant and number of particles to get heat energy, which can be added together. Or you can multiply temperature by duration of time during which it was constant, add these numbers (in K*s) together and divide by total time to get time-average temperature.

Your library provides conversions between different units. Maybe someone will be able to come up with a clever fool-proof type system for thermodynamics or other domains.

@oschonrock
Copy link
Contributor

oschonrock commented Dec 10, 2019

Even more "illogically", it seems that it's sometimes appropriate to "subtract" temperatures:

eg Spec Heat Capacity = m * C * dt

I think these "illogical" exceptions are probably not the concern of a unit library?

@mpusz mpusz closed this as completed Dec 29, 2019
@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 5, 2020

The current approach seems like the right one. By constraining an operator on the representation's operator, units::quantity can work with any algebraic strucutre. Maybe it is the algorithms that should be constrained on algebraic structure concepts. I'm wondering how to fit dimensional analysis into those without having an explosion of concepts.

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 5, 2020

Perhaps one can fit dimensional analysis into those like this:

export template <class T>
concept quantity_field = std::regular<T> && requires(const T& c, T& l) {
//  zero<T>, one<T>, +c, -c, l+=c, l-=c, c+c, c-c
    { c * c } -> std::regular;
    { c / c } -> std::regular;
    { c * c / c } -> std::common_with<T>;
};

export template <class T>
concept field = quantity_field<T> && requires(const T& c, T& l) {
    { l *= c } -> std::same_as<T&>;
    { l /= c } -> std::same_as<T&>;
    { c * c } -> std::common_with<T>;
    { c / c } -> std::common_with<T>;
};

@mpusz
Copy link
Owner Author

mpusz commented Sep 5, 2020

My first try was to reuse http://wg21.link/P01813 but those concepts were overconstrained for our needs. After that, I went with Scalar concepts, and even though this is a really bad name (please help me find a better one) it seems to work nicely here.

It does not mean the Scalar concept is perfect though so if you would like to refine it please, feel encouraged to do so.

@JohelEGP
Copy link
Collaborator

JohelEGP commented Sep 9, 2023

The current approach seems like the right one. By constraining an operator on the representation's operator, units::quantity can work with any algebraic strucutre.

Things have changed since.
We have quantity and quantity_point,
documented to model a vector space and point space, respectively (https://mpusz.github.io/mp-units/2.0/users_guide/framework_basics/the_affine_space/).
So now we know that we require the number to model those (and not just any algebraic structure).
We further support operations on a scalar quantity and modulo.

I have been working on my own number concepts for quite some time now.
I have included them in some of the code fragments I share here.
I once shared the specification at https://cpplang.slack.com/archives/CBGC8H3T7/p1640214020006000.

I recently improved naming based on hsutter/cppfront#231 (comment)
E.g., negatable -> negative representing negative (IEV 102-01-14).
And I concurrently improved the hierarchy based on the discussion at #468.
E.g., I split relative_quantity, which required division,
into vector_space (which doesn't require division)
and f_vector_space (which does require division; search for "F-vector" at https://en.wikipedia.org/wiki/Vector_space).

In case you're interested in how I constrained quantities before,
expand the "Cpp1 quantity" at hsutter/cppfront#658 (comment).
This is how I'm constraining the numbers now: https://godbolt.org/z/W4anGEsss.

  • The number of vector (analogous of mp_units::quantity) is constrained with vector_space,
    representing a vector space (IEV 102-03-01).
  • The number of point (analogous of mp_units::quantity_point) is constrained with point_space,
    representing a point space (IEV 102-03-02).
  • The increment and decrement operators require number_line,
    which aren't subsumed by vector_space or point_space.
    It's the best name I could come up with, based on https://en.wikipedia.org/wiki/Number_line.
  • Addition and subtraction require vector_space_for.
    A more appropriately name might be vector_for_point_space.
    Note that vector_space subsumes point_space.
    We accept anything that behaves like a vector for the point space number.
    E.g., given the vector space int, all integer types behave like a vector for its point space operations.
    This might be more evident in the interface of point.
  • The unary minus is unconstrained because vector_space subsumes negative.
  • Multiplication requires scalar_for to represent a scalar for a vector space.
  • Division requires field_for to represent a scalar for an F-vector.
  • Modulo requires modulus_for to represent the modulus of an integer (or whatever else models it).

There might be room for improving this hierarchy of number concepts:

  • Vector space is defined with a given scalar.
    But in the type system, we have a multitude of scalars.
    E.g., given the F-vector space int, all integer types behave like a scalar for its multiplication and division.
    Vector space also has vector binary operations that result in vectors.
    Again, all integer types behave like vectors with each other.
    How de we name all these concepts?
    Being verbose for clarity, I'm thinking of:
    • vector_space for the base case of a vector space with its "obvious" scalar.
    • vector_space_with_scalar when we accept scalar types for the number (e.g., implementing quantity::operator*).
    • vector_space_with_vector when we accept vector types for the number (e.g., implementing quantity::operator+).
  • Given the previous item,
    I can remove field_for and constrain division with scalar_for_f_vector_space.
    That can be implemented in terms of f_vector_space_with_scalar.
    Note that field_for is minimal.
    It doesn't require that the non-field argument is a vector space.
    That is expected to be part of another, possibly more constrained, requirement.
  • The previous two points might apply to other concepts; definitely to point_space.

Anyways, I think using these concepts is an improvement over the status-quo.

@mpusz
Copy link
Owner Author

mpusz commented Sep 9, 2023

This looks super interesting! 😃
I think it should turn into a PR in order to prototype something and see how it looks, feels, and works with the current code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed Polls
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants