I think I've made a huge mistake (function overloading)

So, I was writing a (personal) library for complex numbers. I know there’s an existing library for that, but some of the syntax kinda bugged me and, wouldn’t you know it, I already made it anyway.

But, I ended up overloading functions quite excessively. I only recently came to the discovery that, in JavaScript, you’re actually not supposed to overload functions. Apparently, it makes the code run slower, and is generally not supported, even though it can still be done. This is…not ideal. Because on top of a library that defines general functions for complex numbers, I also created a separate file which utilizes these functions to define certain special functions for complex arguments (e.g. the Gamma function, the erf function, the dilogarithm, elliptic integrals, etc.) I’ve attached a copy of my complex number library below, and even though it’s pretty long, I’ll summarize in the next paragraph what my main worries are. Because I still don’t know the full details on when function overloading is appropriate in JavaScript.

I overloaded the add, sub, mul, and div functions to work for both complex-with-complex and complex-with-double parameters. Overall, the compiler doesn’t seem to have too much trouble with this; it knows which one’s which. But, I still hear it can cause some problems, as the compiler has to constantly check which version of the function it’s using based on the parameters. In addition, I’m very cautious on whether or not my pow function works right, seeing as how I have three versions (one for complex exponents, one for double exponents, one for integer exponents), and it’s overloaded from a preexisting function for floats, which might make said preexisting function run slower. I’m also unsure on whether or not functions like the equals (which can take different numbers of the same argument) will work properly. They seem to work, but I’m not sure.

Like I said, I only recently discovered I wasn’t supposed to be overloading in java, and am still kinda hesitant to fix this problem, as it seems many of processing’s functions are already overloaded to begin with (rect, str, point), but on the other hand, those might be very specific cases. Help would be much appreciated, thanks!

//////////////////////////////////////////////////////////COMPLEX CLASS////////////////////////////////////////////////////////////////////////////////////////

class complex { //this object represents a complex number
  
//members
  double re, im; //there are only two members of this class: the real and imaginary parts
  
//constructors
  complex()                   { re=im=0;    } //default constructor
  complex(double x, double y) { re=x; im=y; } //normal constructor, inputs of real and imaginary part
  complex(double x)           { re=x; im=0; } //alternate constructor, inputs of real part with imaginary part assumed 0
  
//basic functions
  complex copy()                     { return new complex(re,im);     } //this creates a copy of the complex number on the left (without copying the memory address)
  void set(double x, double y)       { re=x;    im=y;                 } //this allows us to change the values of our complex number
  void set(complex z)                { re=z.re; im=z.im;              } //this allows us to set one complex equal to another, without copying references
  
  boolean equals(complex a)          { return (re==a.re && im==a.im); } //this is true if these two complex numbers are equal
  boolean equals(double x, double y) { return (re==x    && im==y   ); } //this is true if the complex number formed on the inside is equal to the complex # we apply this to
  boolean equals(double x)           { return equals(x,0);            } //this is true if the complex number equals the real input parameter
  
  boolean isInf() { return (Math.abs(re)==inf || Math.abs(im)==inf); } //this is true if the complex number is infinite
  
  //cast complex to a string
  String Str() {                                            //this generates this complex number in text form
  
    if(isInf()) {                                           //special case: input is infinite
      if(Math.abs(im)==inf) { return "Complex Overflow";  } //if the complex part is infinite, return a complex overflow
      else if(re<0)         { return "Negative Overflow"; } //otherwise, if the real part is negative infinity, return a negative overflow
      else                  { return "Overflow";          } //otherwise, return a positive overflow
    }
    
    complex sto=copy();                                                    //this will store the input, but have any uneven roundoff errors removed
    if(Math.abs(re)<1e-12 && Math.abs(im)>1e11*Math.abs(re)) { sto.re=0; } //if the real part is tiny but the imaginary part isn't, remove the real part
    if(Math.abs(im)<1e-12 && Math.abs(re)>1e11*Math.abs(im)) { sto.im=0; } //and vice versa
    
    String ret="";                        //generate and empty string to fill with our number
    
    if(sto.re!=0||sto.im==0) { ret+=str(sto.re); }    //we will display the real part if it's nonzero or if the entire # is 0
    if(sto.re!=0&&sto.im> 0) { ret+="+";         }    //display a + separating the real & imaginary parts ONLY if there is both a real part & a positive imaginary part
    
    if     (str(sto.im).equals("1"))  { ret+="i";             } //imag part is  1; print  i instead of  1i
    else if(str(sto.im).equals("-1")) { ret+="-i";            } //imag part is -1: print -i instead of -1i
    else if(sto.im!=0)                { ret+=str(sto.im)+"i"; } //otherwise, as long as the imaginary part isn't 0, display the imaginary part followed by i
    
    return ret;                           //return the result
  }
  
//basic math functions (single argument)
  double abs2() { return re*re+im*im;       } //absolute square
  double abs()  { return Math.hypot(re,im); } //absolute value
  double arg()  {                             //polar argument
    if(im==0) { return re>=0 ? 0 : Math.PI; }   //real      number: return either 0 or π
    if(re==0) { return sgn(im)*HALFPI;      }   //imaginary number: return ±π/2
    return Math.atan2(im, re);                  //general case: find the angle with atan2
  }
  
  complex neg () { return new complex(-re,-im); } //negation
  complex conj() { return new complex( re,-im); } //complex conjugate
  complex mulI() { return new complex(-im, re); } //multiply by i
  complex divI() { return new complex( im,-re); } //divide   by i
  
  complex inv() {                             //reciprocal
    if(im==0)   { return new complex(1.0/re); } //real      number: return  1/(real part)
    if(re==0)   { return     iTimes(-1.0/im); } //imaginary number: return -i/(imag part)
    if(isInf()) { return new complex();       } //infinite  number: return 0
    
    if(Math.abs(abs2())==inf) {                     //the absolute square is an overflow:
      double k=Math.max(Math.abs(re),Math.abs(im)); //pick the biggest of the two components (let's call this k)
      complex sto2=div(k);                          //divide the number by k
      return sto2.inv().div(k);                     //take the reciprocal, divide back by k
    }
    return conj().div(abs2()); //general case: return the conjugate over the absolute square
  }
  complex sq () { return mul(this); } //z²
  complex cub() { return mul(sq()); } //z³
  
  complex sqrt() { //√(z)
    if(4096*Math.abs(im)>=Math.abs(re)) {                                       //if the imaginary part isn't too small, (NOTE THE EQUALS, TO INCLUDE 0+0i)
      double mag=abs();                                                         //calculate the magnitude
      return new complex(Math.sqrt(mag/2+re/2), sgn(im)*Math.sqrt(mag/2-re/2)); //use the standard formula to find the square root
    }
    //otherwise, the standard formula is imprecise and we must approximate
    
    complex res; //this is our result
    double root=Math.sqrt(Math.abs(re));                         //find the √ of the real part
    if(re>0) { res=new complex(root,im/(2*root));              } //real part is positive:
    else     { res=new complex(im/(2*root),root).mul(sgn(im)); } //real part is negative:
    
    return res.add(div(res)).div(2); //return the mean between res and z/res
  }
  complex exp() { //e^z
    if(im==0) { return new complex(Math.exp(re));              }     //real number : return e^(real part)
    if(re==0) { return new complex(Math.cos(im),Math.sin(im)); }     //imag number : return cos(imag)+sin(imag)*i
    return new complex(Math.cos(im),Math.sin(im)).mul(Math.exp(re)); //general case: return e^(real)*(cos(imag)+sin(imag)*i)
  }
  complex ln() { //ln(z)
    if(re==0||im==0) { return new complex(Math.log(abs()), arg()); } //real/imaginary number: return ln|z|+arg(z)
    
    if(abs2()==inf) {                               //absolute square is an overflow:
      double k=Math.max(Math.abs(re),Math.abs(im)); //pick the biggest of the two components (let's call this k)
      complex sto2=div(k);                          //divide the number by k
      return sto2.ln().add(Math.log(k));            //take the log and add back ln(k)
    }
    return new complex(Math.log(abs2())/2, arg()); //general case: return ln(|z|²)/2+arg(z)i
  }
  
  int csgn() { //complex sgn
    return (re>0 || re==0&&im>=0)?1:-1; //return 1 or -1, depending on if whether z==√(z²)
  }
  
//simple math functions (multivalued)
  complex add(double a) { return new complex(re+a, im  ); } //overload the 4 basic arithmetic operations, both for
  complex sub(double a) { return new complex(re-a, im  ); } //one double & one complex...
  complex mul(double a) { return new complex(re*a, im*a); }
  complex div(double a) { return new complex(re/a, im/a); }
  
  complex add(complex a) { return new complex(re+a.re, im+a.im);                 } //...and for 2 complexes
  complex sub(complex a) { return new complex(re-a.re, im-a.im);                 }
  complex mul(complex a) { return new complex(re*a.re-im*a.im, re*a.im+im*a.re); } //NOTE: there is a way to do this with only 3 products, but it might cause roundoff in some cases
  complex div(complex a) { return mul(a.inv());                                  } //make sure to look into it and have it implemented if/when appropriate
  
  void addeq(complex a) { set(add(a)); } // +=
  void subeq(complex a) { set(sub(a)); } // -=
  void muleq(complex a) { set(mul(a)); } // *=
  void diveq(complex a) { set(div(a)); } // /=
  void negeq()          { set(neg());  } // negate-equals
  
  void muleq(double a)  { set(mul(a)); } // complex*=double
  
  complex pow(int a) { //compute compex z ^ integer a (exponentiation by squaring)
    
    if(im==0) { return new complex(dPow(re,a)); } //input is real: use the other implementation for doubles (resulting in about 1/4 the # of multiplications)
    
    if(a<0)  { return inv().pow(-a); } //a is negative: return (1/z)^(-a)
                                       //general case:
    complex ans=one.copy();         //return value: z^a (init to 1 in case a==0)
    int ex=a;                       //copy of a
    complex iter=this.copy();       //z ^ (2 ^ (whatever digit we're at))
    boolean inits=false;            //true once ans is initialized (to something other than 1)
    
    while(ex!=0) {                               //loop through all a's digits (if a==0, exit loop, return 1)
      if((ex&1)==1) {
        if(inits) { ans.muleq(iter);           } //mult ans by iter ONLY if this digit is 1
        else      { ans.set(iter); inits=true; } //if ans still = 1, set ans=iter (instead of multiplying by iter)
      }
      ex >>= 1;                                  //remove the last digit
      if(ex!=0)   { iter=iter.sq(); }            //square the iterator (unless the loop is over)
    }
    
    return ans; //return the result
  }
  
  complex pow(double a) { //complex z ^ double a
    if(Math.abs(a)<=2147483647 && a%1==0) { return pow((int)a);                 } //if the exponent is an integer, there's a faster (and more accurate) way of doing this
    if(im==0 && (re>0||a%1==0))           { return new complex(Math.pow(re,a)); } //if the base is real and non-negative, or it's negative but the exponent is an integer, we'll just use the built in power function 
    
    double mag, abssq=abs2();                   //we now have to take the a-th power of the magnitude
    if(abssq==inf) { mag=Math.pow(abs(),a  ); } //if the absolute square is an overflow, use the absolute value
    else           { mag=Math.pow(abssq,a/2); } //otherwise, use the absolute square
    complex unit=iTimes(a*arg()).exp();         //create a phaser with angle a*θ
    return unit.mul(mag);                       //return the magnitude times the phaser
  }
  
  complex pow(complex a) {              //complex z ^ complex a
    if(equals(e)) { return a.exp();   } //z==e        : return e^a
    if(a.im==0)   { return pow(a.re); } //a is real   : return complex z ^ double a.re
    return ln().mul(a).exp();           //general case: return e to the power of the log times a
  }
  
  complex cbrt() { //cube root of complex z
    if(im==0) { return new complex(Math.cbrt(re)); }    //z is real   : return cbrt(re(z))
    return iTimes(arg()/3).exp().mul(Math.cbrt(abs())); //general case: return e^(arg(z)/3)*cbrt(|z|)
  }
}

/////////////////////////////////////////////////////////////////////////FRIEND FUNCTIONS////////////////////////////////////////////////////////////////////////////////////////

//here, we set global variables inf=∞, e=euler's number, i=i

double inf=1.0D/0;
double e=2.7182818284590459D;
double HALFPI=1.57079632679489662D;

complex i=new complex(0,1);
complex one=new complex(1);

//we will now overload (most of) the functions from complex so that the complex numbers can be called as parameters

complex iTimes(double x) { return new complex(0,x); } //a "constructor" that returns the input times i

String str(complex z) { return z.Str(); } //this generates the complex number in text form

double re(complex z) { return z.re; } //real & imaginary parts
double im(complex z) { return z.im; }

double abs2(complex z) { return z.abs2(); } //square absolute value
double abs(complex z)  { return z.abs();  } //absolute value
double arg(complex z)  { return z.arg();  } //polar argument

complex conj(complex z) { return z.conj(); } //complex conjugate
complex inv (complex z) { return z.inv();  } //1/z
complex sq  (complex z) { return z.sq();   } //z²
complex cub (complex z) { return z.cub();  } //z cubed

complex sqrt(complex z) { return z.sqrt(); } //√(z)
complex exp(complex z)  { return z.exp();  } //e^z
complex ln(complex z)   { return z.ln();   } //ln(z)

complex add(complex a, double b) { return a.add(b); } //these functions perform the four basic arithmetic
complex sub(complex a, double b) { return a.sub(b); } //operations on one real and one complex number
complex mul(complex a, double b) { return a.mul(b); }
complex div(complex a, double b) { return a.div(b); }

complex add(double a, complex b) { return b.add(a);       } //these functions perform the four basic arithmetic
complex sub(double a, complex b) { return b.neg().add(a); } //operations on one real and one complex number
complex mul(double a, complex b) { return b.mul(a);       }
complex div(double a, complex b) { return inv(b).mul(a);  }

complex add(complex a, complex b) { return a.add(b); } //these functions perform the four basic arithmetic
complex sub(complex a, complex b) { return a.sub(b); } //operations on two complex numbers
complex mul(complex a, complex b) { return a.mul(b); }
complex div(complex a, complex b) { return a.div(b); }

complex pow(complex a, int     b) { return a.pow(b); } //this computes a to the power of an integer
complex pow(complex a, double  b) { return a.pow(b); } //this computes a to the power of a real double
complex pow(complex a, complex b) { return a.pow(b); } //this computes a to the power of another complex number

complex cosh(complex a) {                                         //cosh of complex a
  if(a.im==0)   { return new complex(Math.cosh(a.re));          } //If the input is real, just return the cosh
  if(a.re==0)   { return new complex(Math.cos (a.im));          } //If the input is imaginary, just return the cosine
  if(a.re>709)  { return exp(a.sub(0.6931471805599453D));       } //if the input is too large, return e^(|x|-ln(2))
  if(a.re<-709) { return exp(a.add(0.6931471805599453D).neg()); }
  complex part=exp(a);
  return part.add(part.inv()).div(2);                             //otherwise, return (e^x+e^-x)/2
}
complex sinh(complex a) {                                               //sinh of complex a
  if(a.im==0)   { return new complex(Math.sinh(a.re));                } //If the input is real, just return the sinh
  if(a.re==0)   { return iTimes(Math.sin(a.im));                      } //If the input is imaginary, just return the sine times i
  if(a.re>709)  { return exp(a.sub(0.6931471805599453D));             } //if the input is too large, return ±e^(|x|-ln(2))
  if(a.re<-709) { return exp(a.add(0.6931471805599453D).neg()).neg(); }
  complex part=exp(a);
  return part.sub(part.inv()).div(2);                                   //otherwise, return (e^x-e^-x)/2
}
complex tanh(complex a) {                                     //tanh of complex a
  if(a.im==0) { return new complex(Math.tanh(a.re));     } //input is real      : return the tanh
  if(a.re==0) { return iTimes(Math.tan(a.im));           } //input is imaginary : return the tan times i
  if(Math.abs(a.re)>300) { return new complex(a.csgn()); } //input is very large: return ±1
  complex part=exp(a.mul(2));
  return part.sub(1).div(part.add(1));                     //general case: return (e^(2x)-1)/(e^(2x)+1)
}

complex cos(complex a) { return cosh(a.mulI());        } //3 main trig functions
complex sin(complex a) { return sinh(a.mulI()).divI(); }
complex tan(complex a) { return tanh(a.mulI()).divI(); }

complex acosh(complex a) {                                           //arcosh of complex a
  if(a.im==0&&Math.abs(a.re)<=1) { return iTimes(Math.acos(a.re)); } //a is real between -1 & 1: return the acos times i
  
  if(a.abs2()>1E15D) { //input is very large: return an asymptotic approximation
    complex sto=sq(inv(a)).div(4);
    return ln(a).add(0.6931471805599453D).sub(sto.add(sq(sto).mul(1.5D)));
  }
  return ln(a.add( sqrt(sub(sq(a),1)).mul(a.csgn()) )); //general case: return ln(x+csgn(x)√(x²-1))
}
complex asinh(complex a) {                                           //arsinh of complex a
  if(a.re==0&&Math.abs(a.im)<=1) { return iTimes(Math.asin(a.im)); } //a is imaginary between -i & i: return asin times i
  int sign=a.csgn();
  
  if(a.abs2()>1E15D) { //input is very large: return an asymptotic approximation
    complex sto=sq(inv(a)).div(4);
    return ln(a.mul(sign)).add(0.6931471805599453D).add(sto.sub(sq(sto).mul(1.5D))).mul(sign);
  }
  return ln(a.mul(sign).add(sqrt(add(sq(a),1)))).mul(sign); //general case: return csgn(x)ln(|x|+√(x²+1))
}
complex atanh(complex a) {                        //artanh of complex a
  if(a.re==0) { return iTimes(Math.atan(a.im)); } //a is imaginary: return atan times i
  complex ans=ln(add(1,a).div(sub(1,a))).div(2);  //general case  : atanh(x)=ln((1+x)/(1-x))/2
  if(a.im==0&&a.re>1) { ans=conj(ans); }          //(special case): a is real & >1: take the conjugate
  return ans;                                     //return answer
}

complex acos(complex a) {
  if(a.im==0 && Math.abs(a.re)<=1) { return new complex(Math.acos(a.re)); } //if a is within the bounds, return a simple arccosine
  return asinh(a.mulI()).mulI().add(HALFPI);                                //otherwise, return π/2-arcsine
}
complex asin(complex a) { return asinh(a.mulI()).divI(); } //3 main inverse trig functions
complex atan(complex a) { return atanh(a.mulI()).divI(); }

//I tacked on some double functions here, mostly because I didn't feel like making a whole new tab dedicated to double functions

Double Double(String string) {
  if(string.equals("∞"))  { return  inf; } // ∞: return  ∞
  if(string.equals("-∞")) { return -inf; } //-∞: return -∞
  return new Double(string);               //otherwise, return the number
}

String unsplice(String s, int chop1, int chop2) { //this takes a string and returns the same string with everything between chop1 and chop2 removed
  return s.substring(0, chop1) + s.substring(chop2, s.length());
}

String str(double dub) {  //this converts a double to a string, formatted so it doesn't have too many digits or any leading zeros
  
  //first we check for some special cases:
  if(dub==0)                { return "0";            } //if the number is 0, return "0" (this is specified to make sure "-0" isn't returned)
  else if(dub!=dub)         { return "undefined";    } //if the number is NaN, return "undefined"
  
  //we'll now convert the number into a string, and unsplice from it any trailing zeros
  //(for the purposes of keeping comments short, a decimal point with nothing but zeros after it will also be considered a "trailing zero")
  
  String res; //"RESult"; this is the string we will return
  int cut1;   //position of the 1st cut (right before the first trailing zero)
  int cut2;   //position of the 2nd cut (right after the coefficient)
  
  if(Math.abs(dub)<1e10 && Math.abs(dub)>=1e-3) { //if the number is normal sized, we will return a coefficient w/ no base or exponent
    res=String.format("%1.12f",dub);             //format the number to 12 decimal places
    cut2=res.length();                          //since the text is only a coefficient, cut2 happens at the end of the string
  }
  
  else {                             //number is very large / small: return in full scientific notation
    res=String.format("%1.12E",dub); //format the number to 12 decimal places with scientific notation
    cut2=res.indexOf("E");           //the coefficient ends right before the "E"
    
    //before we move on to remove any trailing zeros, let's first remove any unecessary +s or 0s in the exponent
    if(res.charAt(cut2+2)=='0') { res=unsplice(res,cut2+2,cut2+3); } //if there's a 0 after the "E+" or "E-", remove it
    if(res.charAt(cut2+1)=='+') { res=unsplice(res,cut2+1,cut2+2); } //if there's a + after the "E", remove it
  }
  
  cut1=cut2;                                 //at first, we'll assume there are no trailing zeros
  while(res.charAt(cut1-1)=='0') { cut1--; } //continually decrement cut1 until there are no more zeros before it
  if   (res.charAt(cut1-1)=='.') { cut1--; } //after that, if there's a decimal point before cut1, decrement cut1 once more
  
  if(cut1!=cut2) { res=unsplice(res,cut1,cut2); } //now, we just remove everything between the two cuts (if applicable)
  
  return res; //return the resulting string
}

double dPow(double d, int a) { //compute double d ^ integer a (exponentiation by squaring)
  if(a<0)  { return dPow(1/d,-a); } //a is negative: return (1/d)^(-a)
                                    //general case:
  
  double ans=1;                   //return value, d^a (init to 1 in case a==0)
  int ex=a;                       //copy of a
  double iter=d;                  //d ^ (2 ^ (whatever digit we're at))
  boolean inits=false;            //true once ans is initialized
  
  while(ex!=0) {                          //loop through all of a's digits
    if((ex&1)==1) {
      if(inits) { ans*=iter;            } //multiply ans by iter ONLY if this digit is 1
      else      { ans=iter; inits=true; } //if ans still = 1, set ans=iter (instead of multiplying by iter)
    }
    ex >>= 1;                             //remove the last digit
    if(ex!=0)   { iter*=iter; }           //square the iterator (unless the loop is over)
  }
  
  return ans; //return the result
}

int sgn(double d) { return d>=0 ? 1 : -1; }
1 Like

Do you mean “in Java”? This is posted under Java Processing, and your code is Java. In Java this is called “method overloading” and methods are referred to as having “multiple signatures.” As you note, Processing uses this everywhere – look at a reference page, and you will probably find multiple forms of arguments (multiple signatures, i.e. method overloading) for Processing functions.

It sounds like you have performance concerns. My advice would be to be very cautious about optimizing for a problem that you’ve heard a rumor could exist. The compiler does a lot of optimizing on the way to byte code, so many of your assumptions about what might cause a performance hit could be incorrect. It is almost always better to just test it and see if it does or not.

Also: please consider capitalizing your class name and constructors (complex → Complex) while leaving your methods lower case. It is a convention, but an almost universally practiced one that really helps the readability of your code.

For a test: take your current Complex class and perform a million operations switching back and forth between e.g. the three different equals() signatures, and time it. Run it multiple times and take the average.

Now rename those methods equals1, equals2, and equals3. Peform the same million operations multiple times and take the average.

(and / or do this for add, sub, div, or whatever you are most worried about).

Is there a problem?

3 Likes

Thank you! Quick question, though: If overloading is supported, how come this problem happens?:

At some point, this library used floats instead of doubles. At this time, if I created a function called sqrt. I then, inside the function, asked for the sqrt of a floating point number. The compiler gave me an error, saying “sqrt only accepts parameters sqrt(complex)” (I’m paraphrasing, since I can’t find the exact message). Is this a problem on the error detector’s end, or is it linked to something else?

Without seeing your code, it sounds like you are describing a problem that is mostly unrelated to overloading – “shadowing”.

Processing has a sqrt()

https://processing.org/reference/sqrt_.html

If you create a method within your class, sqrt(), then within your class you have “shadowed” the method in the parent applet, which is no longer accessible within the scope of your class object (it is still accessible in the main sketch) unless you call it explicitly from the sketch object (e.g. p.sqrt()) or load it directly from a library (e.g. Math.sqrt()).

2 Likes

Interesting.

And so, all things considered, there should be no problem whatsoever if I, for example, overload my add and mul functions to also take more than 2 inputs?

It will work. Multiple signatures / overloading is the normal way to do this in Processing and in Processing libraries.

As I mentioned:

  1. If you do this and are concerned about performance, test it

  2. Whether overloaded or not, if you use a method in your class that is also a method name in Processing, you will shadow it, so any access to the parent sketch method needs to be explicit.

3 Likes