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

- Don Clugston <dac nospam.com.au> Oct 06 2005
- "Ben Hinkle" <ben.hinkle gmail.com> Oct 06 2005
- Don Clugston <dac nospam.com.au> Oct 06 2005
- "Ben Hinkle" <bhinkle mathworks.com> Oct 06 2005
- Don Clugston <dac nospam.com.au> Oct 06 2005
- BCS <BCS_member pathlink.com> Oct 06 2005
- Don Clugston <dac nospam.com.au> Oct 06 2005
- Sean Kelly <sean f4.ca> Oct 06 2005

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

"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

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

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

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

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

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

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