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

Templify si.d #4

Closed
wants to merge 15 commits into from
Closed

Templify si.d #4

wants to merge 15 commits into from

Conversation

nordlow
Copy link

@nordlow nordlow commented Jul 12, 2015

Replace version logic with templates.

Compiles without warnings and all tests pass.

I added a few uses of CTFE where I felt it made the code more readable.

Comments please :)

@nordlow nordlow mentioned this pull request Jul 12, 2015
@nordlow nordlow changed the title Templatify si.d Templify si.d Jul 12, 2015
@biozic
Copy link
Owner

biozic commented Jul 12, 2015

Yes it works, but having to write code like below is what I wanted to avoid (assuming N == double would be the default):

auto distance = 384_400 * kilo(meter!());
auto speed = 299_792_458  * meter!()/second!();

Time!() time;
time = distance / speed;
writefln("Travel time of light from the moon: %s s", time.value(second!()));

An alternative would be to put all si.d code into a template mixin (with N as a parameter) and require the user to instantiate it before use.

@biozic
Copy link
Owner

biozic commented Jul 12, 2015

I think I should include the code from examples/synopsis in a proper unittest, so that expected usage is properly checked.

@nordlow
Copy link
Author

nordlow commented Jul 12, 2015

I believe the standard D way of solving this is to replace

enum meter(N = StdN) = unit!(N, "L");

with

auto meter(N = StdN)() { return unit!(N, "L"); };

as long as it's CTFE-able.

I look into this tomorrow.

@nordlow
Copy link
Author

nordlow commented Jul 12, 2015

Good idea with the unittests by the way.

@biozic
Copy link
Owner

biozic commented Jul 12, 2015

It could work, but beware the consequences of having a function instead of a value; for example, Length is defined as:

alias Length = typeof(meter); 

I wonder if forcing the numeric type to be a double, which I think is what is preferred most of the time, would be a much simpler alternative. What do you think ?

@nordlow
Copy link
Author

nordlow commented Jul 12, 2015

I don't think the D developers will approve a solution where precision is "forced". We want this to work with all numeric types including BigInt and user defined types. Why is it important to use Length instead of Length!float?

@biozic
Copy link
Owner

biozic commented Jul 12, 2015

Length!float is ok for me. Something like kilogram!float * meter!float / square(second!float) is quite heavy, but then the user coud just use si!float("kg m s^-2"). We could also require mixin siUnits!float; at the beginning of a module/scope, but I guess it's not very D-ish.

@nordlow
Copy link
Author

nordlow commented Jul 12, 2015

Neither kilogram!float nor meter!float nor second!float will be neeeded as kilogram, meter and second will be instantiator functions with a template argument N that defaults to double. It's only the templated types, such as Length, that have to be explicitly specialized when referenced.

So you will be able to write something like

auto value = 1.0 * kilogram * meter / square(second)

This thanks to D not requiring function calls to arguments to include empty paren pairs, such as kilogram().

This will infer precision of value to be double.

I'll cook up a PR for this in the next couple of days. Ok?

@biozic
Copy link
Owner

biozic commented Jul 12, 2015

I guessed I missed something, so I am waiting for your PR. Thanks for working on this!

@nordlow
Copy link
Author

nordlow commented Jul 13, 2015

You are right. There is no optimal solution to this problem.

The symbol kilogram can only be used in one way, preferrably as an enum instance of a Quantity, as you have already done. I had forgotten that kilogram becomes a function reference in its free form if kilogram is a function as I had planned in my previous mail.

Instead I think it's a good solution to make it double by default as you have already done. Then it's analogous with the precision of numeric literals such as 1.0. If we are to support precisions other than double we need an alternative naming for those symbols. For example

enum meter = unit!(StdN, "L");
auto meters(N = StdN)(N n = 1) { return n*unit!(N, "L"); };

What do you think about the solution that adds a plurals s for the functional (templated) case?

unittest
{
    auto x = meter; // type double
    auto xf = 2.0f.meters; // inferred to type float because 2.0f has type float
    auto yf = meters!float; // inferred to type float because explicit template argument
}

An alternative naming for meters could be meterOf, ofMeter or asMeter.

@biozic
Copy link
Owner

biozic commented Jul 13, 2015

IMO, meter and meters are too close. Also, what about 1 kg∙m/s²?

auto sf = 1.0f.kilograms * 1.0f.meters / square(1.0f.seconds);

Compare to this:

import quantities.si.float_;
auto sf = 1.0f * kilogram * meter / square(second).

I find it more expressive.

How often would the same code use quantities with double, float, int or whatever at the same time? Maybe importing a specialized versions of si.d could be better. Specialized versions of si.d would only mixin a template from a 'sibase' module. A user could build its own version with the same mechanism. Fully-qualified names or aliases could resolve conflicts if they they appear.

I don't know, I am just thinking out loud.

@nordlow
Copy link
Author

nordlow commented Jul 13, 2015

I think these matters will become clear when we create the PR and these questions become discussed with the other Phobos developers in a more "democratic" process. There may be things we are missing.

A more important feature that I'm sure the developers, especially @andralex, will want is support for CommonType. That is, Quantity precisions should be able to mix in arithmetic expressions (by tuning the logic of operator overloading). For example

1.0*unit!(float, "L") * 2.0*unit!(double, "L") * 3.0*unit!(real, "L")

should become

6.0*unit!(real, "L^3")

because CommonType!(float, double, real) is real.

I can start working on this later today.

@biozic
Copy link
Owner

biozic commented Jul 13, 2015

I already tried using CommonType in a previous version, and I reverted it. Because it means every function using quantities as arguments has to be templated.

Speed myFunction(Length len, Time tim) { /* ... */ }

now has to be:

auto myFunction(L, T)(L len, T tim)
   if (AreConsistent!(L, Length) && AreConsistent!(T, Time))
{ /* ... */ }

This is because there is no implicit conversion.

@nordlow
Copy link
Author

nordlow commented Jul 13, 2015

Why is that a problem? Everything else in Phobos is templated.

@biozic
Copy link
Owner

biozic commented Jul 13, 2015

I was talking about client code, not library code.

@nordlow
Copy link
Author

nordlow commented Jul 13, 2015

I just realized that we don't need to use CommonType to promote correct precision in binary operators. We just need to use Quantity!(typeof(result as shown in nordlow@34849b3.

I'm sorry for the reformattings. They weren't intentional.

I don't think our purpose of using CommonType where the same. What was yours? :)

@nordlow
Copy link
Author

nordlow commented Jul 13, 2015

@biozic: BTW, where do you live? :)

@biozic
Copy link
Owner

biozic commented Jul 14, 2015

I live in Dijon, France. Good wines always at hand :)

@biozic
Copy link
Owner

biozic commented Jul 14, 2015

About CommonType and type promotion: I was experimenting all sorts of things on this library until recently, now the purpose is rather to not use it :)

@biozic
Copy link
Owner

biozic commented Jul 14, 2015

About plurals in unit names: plural is ok for units in the numerator, but not in the denominator. You don't say 'meters per seconds'. A solution is to have both aliased. I don't like sentence-like naming personally, and chose to stick to the mathematical definition: quantity = value * unit.

Apart from that, the PR looks good, thanks. I will test it soon.

@nordlow
Copy link
Author

nordlow commented Jul 14, 2015

I'm planning to add a mixin in si.d that for each unit, for instance meter, creates

auto meters(N    = StdN)(N n = 1) { return n * unit!(N, "L"); }
enum meter = meters!StdN;

so we get both both simplicity

enum x = 1.0 * meter; // only double precision
const y = 1.0 * meter; // only double precision

and flexibility

enum x = 1.0.meters!float; // float precision
enum y = 1.0.meters!real; // real precision

@biozic
Copy link
Owner

biozic commented Jul 14, 2015

I really don't like having different semantics between singular and plural.
Also, meters should always mean 1 m, not n m, to be consistant with the rest of the API: it's a unit, after all.

I would balance flexibility with simplicity this way:

  1. Abandon instantiator functions. Use the templated definitions you've proposed. Have a unique definition per SI unit :
enum meter(N) = unit!(N, "L");

Using it directly all the time can feel verbose:

auto speed = 4.5 * kilo(meter!float) / second!float;

but we just need to:
2. Push forward compile-time parsing to remain concise.

enum kms = si!"km/s"; // defining a new unit, using the default double
enum kms = si!("km/s", float); // if the user wants to
Speed!float = si!"4.5 km/s"; // assignation/construction remains simple

Does this contradict your plan ? :-)

@nordlow
Copy link
Author

nordlow commented Jul 14, 2015

Points 1 and 2 are ok to me.

I really like the syntax VALUE.UNIT though. Can we get UCFS-instantiator syntax some other way along with what you have proposed?

@biozic
Copy link
Owner

biozic commented Jul 14, 2015

How do you use this VALUE.UNIT syntax for quantities with compound units?
A strong argument for the 'VALUE * UNIT' is that it follows the physical definition of a quantity more accurately.

@nordlow
Copy link
Author

nordlow commented Jul 15, 2015

We could make the si template an instantiator function aswell, used as

1.0.si!"km/h"

What do you think I the most probable use case

1.0.si!"km/h"

or

x * si!"km/h"

?

I think the answer to that question should decide whether we should make the symbols enums or instantiators.

@biozic
Copy link
Owner

biozic commented Jul 15, 2015

I don't know whether defining constants would be a probable use case or not. Physical constants would be defined once and for all. But I suppose they would still appear quite often in expressions like:

if (speed > 1.0.si!"km/h") { /*...*/ }

In this case, would 1.0.si!"km/h" still be evaluated at compile-time (si!"1 km/h"is)?

For variables, I really don't know what D developers would prefer between

mySpeedValue.si!"km/h"

and

mySpeedValue * si!"km/h"

But both syntaxes are possible if si is an instantiator function, aren't they?

Something worth checking: do all these versions generate the same code, whether in simple or complex expressions? Zero-overhead (compared to using plain numeric types) is needed.

@nordlow
Copy link
Author

nordlow commented Jul 17, 2015

I got another idea. What about making shorthands such as m, s, km, instantiator functions? These should be optional of course. But they don't seem to interfere with local variables at least. This because the following code compiles:

/// Base Units
enum meter   (N = StdN) = unit!(N, "L");
enum kilogram(N = StdN) = unit!(N, "M"); /// ditto
enum second  (N = StdN) = unit!(N, "T"); /// ditto
enum ampere  (N = StdN) = unit!(N, "I"); /// ditto
enum kelvin  (N = StdN) = unit!(N, "Θ"); /// ditto
enum mole    (N = StdN) = unit!(N, "N"); /// ditto
enum candela (N = StdN) = unit!(N, "J"); /// ditto

/// Base Unit Aliases
alias metre = meter; /// ditto

/// Base Units Shorthands
auto m (N = StdN)(N n) { return n * meter!N; }
auto kg(N = StdN)(N n) { return n * kilogram!N; }
auto g (N = StdN)(N n) { return n * gram!N; }
auto s (N = StdN)(N n) { return n * second!N; }
auto A (N = StdN)(N n) { return n * ampere!N; }
auto K (N = StdN)(N n) { return n * kelvin!N; }
auto cd(N = StdN)(N n) { return n * candela!N; }

unittest
{
    auto m = 1.0L; // m can be overloaded as variable
    auto y = m.m; // inferred as real
}

But the syntax 1.0.si!"km/h" is a better default as it's less ambiguous in meaning.

Further, we could make SI!"km/h" refer to the corresponding enum constant (defaulted to double precision) and Si!"km/h" to the type. This kind of conforms to D's symbol naming conventions.

@biozic
Copy link
Owner

biozic commented Jul 19, 2015

Shorthands: no. As you said, the si templates are here for that and more readable, especially for compound units. And what about kilo(m) or units that are not base units like h? (Unless you decide to define all 700+ combination of prefixes and units.)

SI enum constants are not necessary, IMO. If si is defined as auto si(string txt, N = double)(N val = 1), then 42.0.si!"km/h" and 42.0 * si!"km/h" are equivalent (or 42.0f.si!"km/h" and 42.0 * si!("km/h", float)).

I won't be online often for until mid-August. Meanwhile, you can have a look at the definition of symbol and prefix lists and at runtime parsing, that will also be necessary to achieve numeric-type-agnostic 'templatization' :) We will also need tests for each functionaly for different numeric types, and for expressions where they are mixed: I can do this myself later.

@nordlow
Copy link
Author

nordlow commented Aug 4, 2015

But both syntaxes are possible if si is an instantiator function, aren't they?

Yes,

si!"km/h"

works as long as we don't define an enum si aswell.

I say we go with instantiator functions. This enables good shorthands:

1.0f.si!"km/h"
1.0.si!"km/h"
1.0L.si!"km/h"

for different precisions apart from the defaulted case given above.

Se my latest commit for details.

In the light of this, do you still want the the derived units, such as

enum radian(N = stdN) = meter!N / meter!N;

to be enum constants instead of instantiators?

@nordlow
Copy link
Author

nordlow commented Aug 27, 2015

Ping!

I'm eager to pack this into a new Phobos package std.experimental.unit in order to get feedback from Phobos developers.

What do you say?

@biozic biozic closed this Jun 12, 2019
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

Successfully merging this pull request may close these issues.

None yet

2 participants