www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Language flaw: There should be no implicit conversions from non-complex

reply Don Clugston <dac nospam.com.au> writes:
At present (DMD 0.134), the language provides implicit conversions from 
pure real types (float, double, real) and from pure imaginary types 
(ifloat, idouble, ireal) to complex types.

I believe that these implicit conversions are a mistake, and should be 
removed from D. This post is long, because I want to deal with the issue 
comprehensively.

Let's remember that the purpose of implicit conversions is to reduce 
tedious and error-prone casts. To quote type.html:
"It would be tedious to require casts for every type conversion, so 
implicit conversions step in to handle the obvious ones automatically."

In general, implicit conversions from non-complex to complex types 
actually *increases* the number of casts required. We'd be better off 
without them.

The underlying problem is that implicit casts mean that a conversion 
from float or double to real is not considered superior to a conversion 
to creal, so that ambiguities arise very easily.

==========================================================================

Suppose we have the following code.

-- (in std.math ) --
real sin(real x)

-- (in user code) --

real w = sin(0.25);
float f = somefunc(); // maybe it's in a C library

real w2 = sin(f);

--------------------

Now we would like to add a complex sine function to std.math
creal sin(creal x)

and also an optimised version for pure imaginary numbers:
creal sin(ireal x)

And we would like to do it without breaking existing code.

Because of the implicit conversions and the overloading rules,
simply adding the new functions will cause compilation failures.

At present, the only solution is to add:

      real sin(double x) { return sin(cast(real)x); }
      creal sin(idouble x) { return sin(cast(ireal)x); }
      real sin(float x) { return sin(cast(real)x); }
      creal sin(ifloat x) { return sin(cast(ireal)x); }

Note that although the first function accepts a double, it returns a 
real, which is unintuitive.

Note that:
* These functions do not add value. Essentially, we're doing the 
implicit conversions manually. We have completely lost the benefit of 
implicit conversions.
* There is significant use of the cast operator.
* The functions return real even though they have double or float 
arguments, unintuitive, and counter to the C/C++ behaviour.
* There are new opportunities for subtle bugs. Especially, you could type:

double sin(double x)
which looks correct, compiles without warning, but inadvertantly changes 
the behaviour of existing code.

Case in point:
sqrt(2.0) used to return a real. Now it returns a double, because the 
defined function is
double sqrt(double x)
rather than
real sqrt(double x)

So the precision has silently decreased.

==========================================================================
  When are the existing implicit conversions helpful?

I can think of three scenarios where they are useful. But for each of them,
I don't think the benefits are convincing.

(a) when you only have a function that accepts a creal,

creal func(creal)

you can currently write

creal a = func(2.0);
creal b = func(2.0i);

without needing to provide a seperate overload for real and ireal.

(b) you can set a creal equal to a function that returns a real or ireal.

ireal ifunc();

creal c = ifunc();

But, there are issues with both of these conveniences.
* Almost always, there are optimisation opportunies for pure real and 
pure imaginary arguments. A version taking a real argument will normally 
be provided, so case (a) will be very rare in practice.

* There are TWO ways to convert from real x to creal z.
       z = x + 0.0i;
       z = x - 0.0i;
      and similarly for imaginary ireal y:
      z =  0.0 + y;
      z = -0.0 + y;
   In case (a), when the argument is a negative real, the compiler is 
quite probably choosing which side of the branch cut you will get. It's 
arguably better to make the programmer spell this out.
   Note that conversion from real/ireal to creal can be done without a 
cast, you just need to add 0.0 or 0.0i.


This only leaves case (c).
(c) a function that contains multiple creal parameters.

void multifunc(creal a, creal b)

At present, you can write
multifunc(2.0, 3.0i);
but if the implicit conversions were disallowed, you would have to use 
either:
multifunc(2.0 + 0i, 0 + 3i);
Or declare:
void multifunc(real, creal), void multifunc(real, real), void 
multifunc(real, ireal),
void multifunc(ireal, creal), void multifunc(ireal, real), void 
multifunc(ireal, ireal),
void multifunc(creal, real), void multifunc(creal, ireal)

-- and it gets worse with multiple parameters.

However, if in this case you had overloaded it with
multifunc(real a, real b)
you already have to provide multifunc(double, creal), multifunc(float, 
creal),
multifunc(creal, double), multifunc(creal, float), multifunc(double, 
double),
multifunc(float, float)

so the benefit is not large.
Moreover, if you have multifunc(real, real) and multifunc(creal, creal)
and you want
double a, b;
multifunc(a, b);
to call the real version, with implicit conversions, you have to change 
the call to
multifunc(cast(real)(a), cast(real)(b));
but without implicit conversions, it works automatically.

=================================================================

In summary -- implicit conversions from pure real or pure imaginary, to 
complex types, are not helpful. They make things more difficult, not less.

For me, these implicit conversions render D's complex numbers almost 
unworkable at the present time.
Oct 06 2005
next sibling parent reply "Ben Hinkle" <ben.hinkle gmail.com> writes:
"Don Clugston" <dac nospam.com.au> wrote in message 
news:di2p73$2mgj$1 digitaldaemon.com...
 At present (DMD 0.134), the language provides implicit conversions from 
 pure real types (float, double, real) and from pure imaginary types 
 (ifloat, idouble, ireal) to complex types.

 I believe that these implicit conversions are a mistake, and should be 
 removed from D. This post is long, because I want to deal with the issue 
 comprehensively.

You make a good argument. It seems reasonable that complex and non-complex versions of the same functions will exist and that will cause clashes and ambiguous overloads if imported together. One way out if the implicit conversions are to stay is to have real and complex versions in different modules and to recommend only importing one of the two modules. The complex module can private import the real functions and alias them to different names. For example std.math real sqrt(real x); std.cmath private import math; creal sqrt(creal z); alias std.math.sqrt re_sqrt; test: import cmath; int main() { cdouble z = 4+5i; cdouble w = sqrt(z); // complex version cdouble p = sqrt(10); // complex version double q = re_sqrt(p.re); // real version return 0; } That seems pretty natural to me to consider the complex versions as "subsuming" the real ones. By that I mean once you start using complex math the standard names like sqrt and sin etc will mean the complex ones and to get the real ones you need to explicitly ask for it. If you never use complex math, though, you just import std.math and sqrt and sin mean the real versions.
Oct 06 2005
parent reply Don Clugston <dac nospam.com.au> writes:
Ben Hinkle wrote:
 "Don Clugston" <dac nospam.com.au> wrote in message 
 news:di2p73$2mgj$1 digitaldaemon.com...
 
At present (DMD 0.134), the language provides implicit conversions from 
pure real types (float, double, real) and from pure imaginary types 
(ifloat, idouble, ireal) to complex types.

I believe that these implicit conversions are a mistake, and should be 
removed from D. This post is long, because I want to deal with the issue 
comprehensively.

You make a good argument. It seems reasonable that complex and non-complex versions of the same functions will exist and that will cause clashes and ambiguous overloads if imported together. One way out if the implicit conversions are to stay is to have real and complex versions in different modules and to recommend only importing one of the two modules. The complex module can private import the real functions and alias them to different names. For example std.math real sqrt(real x); std.cmath private import math; creal sqrt(creal z); alias std.math.sqrt re_sqrt; test: import cmath; int main() { cdouble z = 4+5i; cdouble w = sqrt(z); // complex version cdouble p = sqrt(10); // complex version double q = re_sqrt(p.re); // real version return 0; } That seems pretty natural to me to consider the complex versions as "subsuming" the real ones. By that I mean once you start using complex math the standard names like sqrt and sin etc will mean the complex ones and to get the real ones you need to explicitly ask for it. If you never use complex math, though, you just import std.math and sqrt and sin mean the real versions.

That's possible. But it would probably make more sense to call the complex functions csqrt(), csin(), etc. What you're doing is manually preventing the compiler from getting the oportunity to perform an implicit conversion. Again, the implicit conversions are just getting in the way. It is common for real and complex functions to be used at once. For example, here's the definition of complex sinh: // sinh(x+iy) = Re[exp(iy)] * sinh(x) + Im[exp(iy)]*cosh(x)i creal sinh(creal z) { creal cis = exp(z.im * 1i); return cis.re * sinh(z.re) + cis.im * cosh(z.re) * 1i; } --- uses exp(ireal), sinh(real) and cosh(real), but obviously sinh(creal) is in the same scope. // Or (less efficient) creal sinh(creal z) { return cos(z.im) * sinh(z.re) + sin(z.im) * cosh(z.re) * 1i; } As defined above, I think it's a fantastic example of how great D is for complex arithmetic -- straightforward and highly efficient. But it's not possible while the implicit conversions exist. Using re_sinh() or csinh() would spoil the beauty.
Oct 06 2005
parent reply "Ben Hinkle" <bhinkle mathworks.com> writes:
 That's possible. But it would probably make more sense to call the complex 
 functions csqrt(), csin(), etc.
 What you're doing is manually preventing the compiler from getting the 
 oportunity to perform an implicit conversion.
 Again, the implicit conversions are just getting in the way.

It's debatable what is getting in the way: the implicit conversion or the ambiguous overloading. A downside of removing the implicit conversion is that the expressions z+1 or z+1.0 for complex z become illegal.
 It is common for real and complex functions to be used at once.

My personal experience from my phd work in complex dynamics (I'm biased I guess - for reference my only publication is in http://journals.cambridge.org/action/displayIssue?jid=ETS&vol meId=20&issueId=01) is that once a problem enters the complex numbers they tend to dominate.
 For example, here's the definition of complex sinh:

 // sinh(x+iy) = Re[exp(iy)] * sinh(x) + Im[exp(iy)]*cosh(x)i
 creal sinh(creal z)
 {
   creal cis = exp(z.im * 1i);
   return cis.re * sinh(z.re) + cis.im * cosh(z.re) * 1i;
 }

 --- uses exp(ireal), sinh(real) and cosh(real), but obviously sinh(creal) 
 is in the same scope.

 // Or (less efficient)
 creal sinh(creal z)
 {
   return cos(z.im) * sinh(z.re) + sin(z.im) * cosh(z.re) * 1i;
 }

 As defined above, I think it's a fantastic example of how great D is for 
 complex arithmetic -- straightforward and highly efficient. But it's not 
 possible while the implicit conversions exist.
 Using re_sinh() or csinh() would spoil the beauty.

Defining sinh or other special math functions is rare (in particular it happens only in std.cmath pretty much). Mixing complex and real sinh in user code I believe will also be rare.
Oct 06 2005
parent reply Don Clugston <dac nospam.com.au> writes:
Ben Hinkle wrote:
That's possible. But it would probably make more sense to call the complex 
functions csqrt(), csin(), etc.
What you're doing is manually preventing the compiler from getting the 
oportunity to perform an implicit conversion.
Again, the implicit conversions are just getting in the way.

It's debatable what is getting in the way: the implicit conversion or the ambiguous overloading. A downside of removing the implicit conversion is that the expressions z+1 or z+1.0 for complex z become illegal.

That would be true, except that the operations creal + real, creal+ireal, etc are already defined. They were created to improve efficiency; but this optimisation has the interesting side-effect that implicit conversions to complex are not required in expressions (where they would be useful), they are only required for function resulution (where they are not useful).
It is common for real and complex functions to be used at once.

My personal experience from my phd work in complex dynamics (I'm biased I guess - for reference my only publication is in http://journals.cambridge.org/action/displayIssue?jid=ETS&vol meId=20&issueId=01) is that once a problem enters the complex numbers they tend to dominate.
For example, here's the definition of complex sinh:

// sinh(x+iy) = Re[exp(iy)] * sinh(x) + Im[exp(iy)]*cosh(x)i
creal sinh(creal z)
{
  creal cis = exp(z.im * 1i);
  return cis.re * sinh(z.re) + cis.im * cosh(z.re) * 1i;
}

--- uses exp(ireal), sinh(real) and cosh(real), but obviously sinh(creal) 
is in the same scope.

// Or (less efficient)
creal sinh(creal z)
{
  return cos(z.im) * sinh(z.re) + sin(z.im) * cosh(z.re) * 1i;
}

As defined above, I think it's a fantastic example of how great D is for 
complex arithmetic -- straightforward and highly efficient. But it's not 
possible while the implicit conversions exist.
Using re_sinh() or csinh() would spoil the beauty.

Defining sinh or other special math functions is rare (in particular it happens only in std.cmath pretty much). Mixing complex and real sinh in user code I believe will also be rare.

You're right, I've got a biased perspective, having just tried to implement those functions :) This is probably true for trig and hyp trig functions, but in the past, I've certainly used complex exp and real exp in the same file. Most of my experience is with predominantly real functions, with an occasional use of complex. Generally the complex numbers pop up briefly, a complex conjugate is used, and then we're back into reals. So you can get (for example) complex exp() with real sin() and cos(). Even if in this file, exp() is always complex, sin() is always real, but the simple seperation into real and complex math doesn't really work.
Oct 06 2005
parent reply BCS <BCS_member pathlink.com> writes:
One solution comes to mind, for simple operators (+,-,*,/) donít use implicit
casts between ANY of real, imaginary or complex types. Instead define the type
of simple actions based on the return type. 

Example (for +):
// legal
real + ireal = creal
creal + real  = creal
ireal + creal =  creal

void fn(creal);
real r;
ireal i;

// illegal
fn(r);
fn(i);

and similarly for the other operators.

If this is the way things are done now or has some other major flaw, please
donít hesitate to make me feel like a fool. ;-)



In article <di3gi0$17ub$1 digitaldaemon.com>, Don Clugston says...
That would be true, except that the operations creal + real, 
creal+ireal, etc are already defined. They were created to improve 
efficiency; but this optimisation has the interesting side-effect that 
implicit conversions to complex are not required in expressions (where 
they would be useful), they are only required for function resulution
(where they are not useful).

Oct 06 2005
parent Don Clugston <dac nospam.com.au> writes:
BCS wrote:
 One solution comes to mind, for simple operators (+,-,*,/) donít use implicit
 casts between ANY of real, imaginary or complex types. Instead define the type
 of simple actions based on the return type. 
 
 Example (for +):
 // legal
 real + ireal = creal
 creal + real  = creal
 ireal + creal =  creal
 
 void fn(creal);
 real r;
 ireal i;
 
 // illegal
 fn(r);
 fn(i);
 
 and similarly for the other operators.
 
 If this is the way things are done now or has some other major flaw, please
 donít hesitate to make me feel like a fool. ;-)

Based on dmd/html/d/cppcomplex.html, I think this is the way complex *expressions* are supposed to be done now. (But, all the stuff in "Comparisons with other languages" is very old, and I don't know if it is still correct, or even implemented). If there were no implicit conversions, the behaviour with function overloading would be exactly as you've described. Combining everyones comments to date, I think we can say: * mixing real and complex calculations together is relatively uncommon, so it's rare for implicit conversion to be a possibility; * the most important cases (expressions) are already treated specially, for reasons of efficiency; * in most other situations, implicit conversion is just a nuisance. If it was removed, then implicit conversion would only ever be used for increasing the precision of a type.
Oct 06 2005
prev sibling parent Sean Kelly <sean f4.ca> writes:
In article <di2p73$2mgj$1 digitaldaemon.com>, Don Clugston says...
At present, the only solution is to add:

      real sin(double x) { return sin(cast(real)x); }
      creal sin(idouble x) { return sin(cast(ireal)x); }
      real sin(float x) { return sin(cast(real)x); }
      creal sin(ifloat x) { return sin(cast(ireal)x); }

For what it's worth, implicit template instantiation would make this problem go away also. But you make a convincing point either way. Sean
Oct 06 2005