std.rational -- update and progress towards review
Joseph Rushton Wakeling
joseph.wakeling at webdrake.net
Thu Oct 3 03:39:14 PDT 2013
On 02/10/13 19:37, H. S. Teoh wrote:
>> * Because std.rational doesn't just want to work with built-in
>> integer types it can't rely on the existing isIntegral.
>
> TBH, I found isIntegral rather counterintuitive. I thought it would
> evaluate to true with BigInt, but it doesn't. If I had my way, I'd
> propose renaming isIntegral to isBuiltInIntegral, and David's
> isIntegerLike to isIntegral.
It's a fair thought, but at this point I guess we have to consider whether
people may be using isIntegral specifically to check for built-in integer type.
(I seem to remember someone -- Bearophile? -- filed an enhancement request for
isIntegral to expand its scope to include BigInts, but searching D bugzilla now
I can't find it. Maybe it was just a forum discussion.)
> Maybe we could have a bit of discussion here in the forum on all of the
> proposed additions to std.traits, then once that decision is made, put
> up std.rational for the actual "official" review?
That's what I was hoping for -- get all the "what goes where" decisions out of
the way first, then there's less to worry about in the official review.
> Since Rational is a templated type, no function attributes are needed.
> The compiler should be able to infer the appropriate attributes.
>
> Unless, of course, you find a case where it makes sense to constrict any
> future changes in implementation (e.g., guarantee that gcd is always
> pure -- but even that is questionable since gcd's purity would depend on
> the purity of operations on the type it is being instantiated for, so
> even in this case I'd say keep it unmarked and let attribute inference
> do its job).
Ahh, OK. I don't feel 100% on what the compiler can infer attribute-wise
compared to what needs to be explicitly written.
>> The remaining open issues
>> <https://github.com/WebDrake/Rational/issues?state=open> are all
>> design-related. Apart from those raised by my above queries, the
>> major one is how rationals should relate to floating-point numbers --
>> e.g. there is currently no opCmp for floating-point, meaning this:
>
>>
>> assert(rational(10, 1) == 10);
>>
>> ... will work, but this:
>>
>> assert(rational(10, 1) == 10.0);
>>
>> ... will fail to compile. It's not entirely obvious how to resolve
>> this as floating-point vs. rational comparisons risk accidentally
>> creating huge temporary BigInt-based rationals ... :-(
>
> Does addition/subtraction with floating point work correctly? If so, the
> user should simply write:
>
> assert(abs(rational(10,1) - 10.0) < EPSILON);
Well, yes, obviously one can use this formalism. On that note, approxEqual
won't work with rationals, e.g.:
auto r1 = rational(10);
assert(approxEqual(r1, 10.0));
fails with error message:
/opt/dmd/include/d2/std/math.d(5689): Error: function std.math.fabs (real
x) is not callable using argument types (Rational!int)
/opt/dmd/include/d2/std/math.d(5696): Error: incompatible types for ((lhs)
- (rhs)): 'Rational!int' and 'double'
/opt/dmd/include/d2/std/math.d(5697): Error: incompatible types for ((lhs)
- (rhs)): 'Rational!int' and 'double'
/opt/dmd/include/d2/std/math.d(5707): Error: template instance
std.math.approxEqual!(Rational!int, double, double) error instantiating
rational.d(57): instantiated from here: approxEqual!(Rational!int,
double)
rational.d(57): Error: template instance
std.math.approxEqual!(Rational!int, double) error instantiating
(Ignore the line number, it's a temporary unittest I knocked up just to try this
out just now and is not in the repo.)
You can do approxEqual(cast(real) r1, float1), however.
> We should definitely not convert floats to Rational just so they can be
> compared, because floats are inexact by definition, whereas Rationals
> are always exact. For example, rational(2,10) != 0.2f, because 0.2f has
> no exact representation in any binary floating-point format. But if you
> support rational(10,1) == 10.0, then people will expect that
> rational(2,10) == 0.2 should also work, but it *can't* work. We should
> not sweep these issues under the rug, but force the user to come to
> terms with the nature of floating-point numbers.
Well, the point is that anyone who knows anything about floating point knows
that comparisons of the form float1 == float2 or float1 == int1 are dangerous
because tiny rounding errors can result in the floating-point number being ever
so slightly off. But you're not _banned_ from making the comparison; the code
won't fail to compile.
So, it feels bad that there isn't an opCmp for floating-point, even though I can
see logical reasons for that. After all, it's one thing that you can't
guarantee an opEquals, another that you can't do something like
auto r1 = rational(2, 3);
assert(r1 < 0.8);
> OTOH, one compromise might be to allow implicit conversion of Rationals
> to floating-point via alias this:
>
> struct Rational(T) {
> ...
> @property real toReal() { return this.convertToReal(); }
> alias toReal this;
> }
>
> void main() {
> float x = 1.0;
> assert(rational(1,1) == x);
> }
There is already an opCast for floating point, so you could define an opEquals
that does something like:
int opEquals(Rhs)(Rhs rhs)
if (isFloatingPoint!Rhs)
{
return (cast(real) this) == rhs;
}
(Ad-hoc knock-up here for example purposes, have not tried or tested:-)
> This works because Rational implicitly converts to real, and x also
> implicitly converts to real, and both are then comparable.
The trouble is that this is ultimately selling the user a false promise, because
the cast to real is in general approximating rather than equalling the rational
number. :-(
More information about the Digitalmars-d
mailing list