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

feat!: composable symbols #520

Open
JohelEGP opened this issue Nov 27, 2023 · 11 comments
Open

feat!: composable symbols #520

JohelEGP opened this issue Nov 27, 2023 · 11 comments

Comments

@JohelEGP
Copy link
Collaborator

JohelEGP commented Nov 27, 2023

feat!: composable symbols

My original intent with "symbols" was to have composable 1-letter symbols that can describe units.
For example, m for metre and mm for millimetre, all with the same m in code.
Instead, we got the product "unit prefix × unit symbol".
So on top of m, we have mm and m3.

I think we should explore my original intent.
I'm not suggesting to remove mm or m3.
What I'm suggesting is to enable defining mm in terms of m.
This allows users to not have to pre-define prefixed versions of their units.

master feat!
x * m x * m
x * mm x * m(m)

There's only one m (per system, so x * m keeps meaning "x SI metres").
Its meaning is contextual, much like in paper calculations.
The rules for composition are described in the SI Brochure §3 "Decimal multiples and sub-multiples of SI units".
Although m.m is a possible syntax, m(m) supports program-defined units such as M(px).

Abusing ^ for power:

master feat! clang-formats as
x * m3 x * (m^3) x * (m ^ 3)
N/A x * m(m^3) x * m(m ^ 3)

Abusing ^ for power would require constexpr parameters.
Lately, there's interest in that direction: cplusplus/papers#1458 (comment).

Examples:

An implementation using the version 2.0.0: https://godbolt.org/z/cd8qjrdxe.

#include <mp-units/systems/si/si.h>

using namespace mp_units;
using namespace mp_units::si::unit_symbols;

namespace ns {
  template<template<PrefixableUnit auto U> class Prefix, Unit auto U> struct prefix_or_unit : decltype(U) {
    template<PrefixableUnit U2> consteval Unit auto operator()(U2) const { return Prefix<U2{}>{}; }
  };
  inline constexpr struct m : prefix_or_unit<si::milli_, ::m> { } m;
}

static_assert(2 * m == 2 * ns::m);
static_assert(2 * mm == 2 * ns::m(ns::m));
master feat!
include/mp-units/systems/si/unit_symbols.h
inline constexpr auto mm = milli<metre>; inline constexpr auto mm = m(m);
example/spectroscopy_units.cpp
isq::frequency(1. * THz) isq::frequency(1. * T(Hz))
isq::wavelength(1. * um) isq::wavelength(1. * u(m))
example/total_energy.cpp
sqrt(pow<2>(p * c) + pow<2>(m * pow<2>(c))) ^ for quantities too?
isq::momentum(4. * GeV / c) isq::momentum(4. * G(eV) / c)
QuantityOf<isq::mass> auto m1 = 3. * GeV / c2 QuantityOf<isq::mass> auto m1 = 3. * G(eV) / (c^2)
p1.in(kg * m / s) p1.in(k(g) * m / s)
@mpusz
Copy link
Owner

mpusz commented Nov 27, 2023

I have mixed feelings about this feature. On one side I admire the cleverness, on the other side I think it tries to be too clever ;-)

I also am not sure what is the intent. Initially I thought it is to limit the number of definitions needed for the standardization process but you stated that "I'm not suggesting to remove mm or m3." If all the prefixed symbols have to remain than what is the rationalle for this feature? User always may write:

constexpr auto mm = si::milli<si::metre>;

and use it with a short name without the need to include all the "garbage" from unit_symbols namespace. What is the benefit of the new syntax?

@mpusz
Copy link
Owner

mpusz commented Nov 27, 2023

Yes, if we get constexpr parameters we could make m^3 work out of the box without the need for a short but arbitrary list of powered units.

@JohelEGP
Copy link
Collaborator Author

I also am not sure what is the intent. Initially I thought it is to limit the number of definitions needed for the standardization process but you stated that "I'm not suggesting to remove mm or m3." If all the prefixed symbols have to remain than what is the rationalle for this feature?

Yeah, my original intention was to never even have a symbol like mm in the library.
And I was going to propose removing them in this issue,
which is why there's a leftover "!" for "BREAKING CHANGE".
But I noticed that there is a standalone benefit:

This allows users to not have to pre-define prefixed versions of their units.

@mpusz
Copy link
Owner

mpusz commented Nov 27, 2023

I thought about having helpers like that before but decided that:

quantity q = 42 * k(m);

is too much syntax noise and it is actually not that easy to read at all. If this was the only thing exposed in the library interface I would probably encourage in the docs to the following anyway:

constexpr auto km = k(m);

And then it is not much different from:

constexpr auto km = si::kilo<si::metre>;

or

constexpr auto km = si::kilo<m>;

so having those additional short helpers does not really buy us anything.

@JohelEGP
Copy link
Collaborator Author

Yeah, k(m) isn't as readable as km.
With UFCS, we could make it k.m() to bring the symbols closer
at the expense of 3 extra characters compared to km.

If this was the only thing exposed in the library interface I would probably encourage in the docs to the following anyway:
And then it is not much different from:
or
so having those additional short helpers does not really buy us anything.

That's because you're defining them or encouraging them.
You could not define them and instead encourage composing symbols.

The difference between being able to do k(m) and not is requiring to pre-define such combined symbols.

Consider if I defined px as an unit of pixel.
Someone might be interested in Mpx for Megapixel.
Others might be interested in more esoteric multiples.
Others yet could write generic interfaces that operate on symbols.

To achieve mp-units level of API, I would have to pre-define pixel for all SI multiples.

For generic programming, I mean things like

template<PrefixableUnit auto U> auto my_algo(…) { … 42 * kilo<U> … } // Today.
template<PrefixableUnit auto U> auto my_algo(…) { … 42 * k(U) … }    // Proposed.

I have no concrete use cases of such a generic interface in mind, though.

@mpusz
Copy link
Owner

mpusz commented Nov 27, 2023

Someone might be interested in Mpx for Megapixel.
Others might be interested in more esoteric multiples.

Sure, adding such a symbol is a short, simple line, as I stated above. So, even if you do not predefine scaled units, a user may easily add those. I don't like the syntax of 42 * M(px), and I would prefer to see 42 * si::mega<pixel> in the code. I also think that:

constexpr auto Mpx = M(px);

is not a big win over:

constexpr auto Mpx = si::mega<px>;

At least not big enough to excuse adding a new abstraction to the library.

@mpusz
Copy link
Owner

mpusz commented Nov 27, 2023

BTW, we can discuss again if prefixes should be variable templates or functions. I personally prefer variable templates as those in the code resemble better types visible in compile-time errors. Functions on the other hand could benefit from ADL as you stated some time ago.

@chiphogg
Copy link
Collaborator

chiphogg commented Dec 2, 2023

I had thought about adding prefix symbols to Au a while ago. I quickly dismissed it as unworkable because m is ambiguous: just consider the mm unit (m(m)?) as a concise example. However, this PR shows that I was too hasty in my dismissal, thanks to the stunning and delightful insight that the context can disambiguate the meaning: m can be an object that acts as a prefix symbol when called, and acts as a unit symbol otherwise. This was really creative!

That said, I think my overall opinion is in line with this comment:

Sure, adding such a symbol is a short, simple line, as I stated above. So, even if you do not predefine scaled units, a user may easily add those. I don't like the syntax of 42 * M(px), and I would prefer to see 42 * si::mega<pixel> in the code. I also think that:

constexpr auto Mpx = M(px);

is not a big win over:

constexpr auto Mpx = si::mega<px>;

At least not big enough to excuse adding a new abstraction to the library.

Expanding on this: the common way to bring in a symbol will be a single using declaration per symbol at the top of a .cc file. Making a new symbol is also one concise line, although it looks slightly different. So for example (and using Au syntax because it's what I know from memory):

using au::symbols::h;
using au::symbols::m;

// Bringing in a `km` symbol.
//
// With currently available methods:
constexpr auto km = kilo(m);
// With prefix symbols:
constexpr auto km = k(m);

std::cout << v.as(km / h);

The only potential benefit I could see is if we want to enable people to stop defining these symbols in the first place, and instead compose them on the fly. That's genuinely novel and not something we could do today:

std::cout << v.as(k(m) / h);

However, we already need to add a line for each pre-existing symbol that we want to use, so it's probably simplest to do this for the composed symbols as well. While I'm very impressed by the creativity, I agree that it probably doesn't justify the added complexity of what something like m would become.

@chiphogg
Copy link
Collaborator

chiphogg commented Dec 2, 2023

By the way: I was surprised to see every single prefixed symbol defined in the library. This strikes me as a lot of extra work, a lot of "paying for what you don't use". Given that:

  • You need to spend one line to get the symbol either way, whether importing a pre-existing name or constructing a new one;
  • And, one-line construction by applying a prefix is easy and natural;

I think we'd be better off deleting all of the prefixed symbols, and telling users how to define their own. (This is Au's approach.)

@mpusz
Copy link
Owner

mpusz commented Dec 2, 2023

I think we'd be better off deleting all of the prefixed symbols, and telling users how to define their own.

I am not sure which approach is better, and for sure we should provide those as alternatives in P3045. I am sure that plenty of people would like to have things like km built it. On the other hand, I know that the definitions take up a lot of space. However, the library is going to be provided as a C++ module and not as a header file. Maybe it is better to pay for it once at module compilation than ask users to redefine them all over again.

@JohelEGP
Copy link
Collaborator Author

JohelEGP commented Dec 2, 2023

However, we already need to add a line for each pre-existing symbol that we want to use, so it's probably simplest to do this for the composed symbols as well.

With composable symbols, you don't need to do that.
In the rare cases where you need multiple multiples of units,
composing symbols of the fly save you from pre-defining N-1 prefixed units.

For example, today:

void f() {
  using lib::h;
  using lib::m;

  auto km = lib::kilo<m>;
  auto mm = lib::milli<m>;

  … km … mm … h …;
}

With composable symbols:

void f() {
  using lib::h;
  using lib::m;

  … k(m) … m(m) … h …;
}

You don't even need to introduce k if you make if work with ADL
(although ADL can't work with a program-defined prefix and a library-defined unit).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants