I'm currently working on a library that provides a mathematical interface for different operations. Operations like solving an equation or calculate derivatives of a function. With a recent update in the requirements, I have to include Scalar and Complex numbers, as well as Vectors and Matrices. With this second part, I came up with a rough, not so elegant - to say the least - solution. To be honest, I don't see how I could simplify things. Here's my current approach: Extending the Number class, I added Scalar and Complex types:
public final class Scalar extends Number {
private String real;
public Scalar(String real) {
this.real = real;
}
public Scalar(Double real) { this.real = real.toString(); }
@Override
public int intValue() {
return Double.valueOf(real).intValue();
}
@Override
public long longValue() {
return Double.valueOf(real).longValue();
}
@Override
public float floatValue() {
return Double.valueOf(real).floatValue();
}
@Override
public double doubleValue() {
return Double.valueOf(real);
}
@Override
public String toString() {
return real.toString();
}
}
public class Complex extends Number {
private Scalar real,
imaginary;
public Complex(String real) {
this.real = new Scalar(real);
this.imaginary = new Scalar("0");
}
public Complex(Double real) {
this.real = new Scalar(real);
this.imaginary = new Scalar("0");
}
public Complex(String real, String imaginary) {
this.real = new Scalar(real);
this.imaginary = new Scalar(imaginary);
}
public Complex(Double real, Double imaginary) {
this.real = new Scalar(real);
this.imaginary = new Scalar(imaginary);
}
public Double imaginary() {
return imaginary.doubleValue();
}
@Override
public int intValue() {
return real.intValue();
}
@Override
public long longValue() {
return real.longValue();
}
@Override
public float floatValue() {
return real.floatValue();
}
@Override
public double doubleValue() {
return real.doubleValue();
}
@Override
public String toString() {
return String.format("%s%c%si",
real.toString(),
imaginary.intValue() < 0 ? '-' : '+',
imaginary.intValue() < 0 ? -1.0 * imaginary.doubleValue() :
imaginary.doubleValue());
}
}
Don't mind with the toString implementations, they're only there for debugging purposes and will be removed later. For simplicity's sake I haven't included Vector/Matrix types at this stage. Now, I need a way to define operators for these classes.
public class Calculator {
public static
<T extends Number,
U extends Number>
Number add(final T lhs,
final U rhs)
throws ArithmeticException
{
Class<? extends Number> lClass = lhs.getClass(),
rClass = rhs.getClass();
if (Scalar.class.equals(lClass) && Scalar.class.equals(rClass)) {
return addScalar((Scalar) lhs, (Scalar) rhs);
}
if (Complex.class.equals(lClass) && Complex.class.equals(rClass)) {
return addComplex((Complex) lhs, (Complex) rhs);
}
if (Complex.class.equals(lClass) && !Complex.class.equals(rClass)) {
return addScalarToComplex((Scalar) rhs, (Complex) lhs);
}
if (Complex.class.equals(rClass) && !Complex.class.equals(lClass)) {
return addScalarToComplex((Scalar) lhs, (Complex) rhs);
}
throw new ArithmeticException();
}
private static Scalar addScalar(final Scalar lop, final Scalar rop) {
return new Scalar(lop.doubleValue() + rop.doubleValue());
}
private static Complex addComplex(final Complex lop, final Complex rop) {
return new Complex(lop.doubleValue() + rop.doubleValue(),
lop.imaginary() + rop.imaginary());
}
private static Complex addScalarToComplex(final Scalar lop, final Complex rop) {
return new Complex(lop.doubleValue() + rop.doubleValue(), rop.imaginary());
}
}
Now this, is my main concern. It feels horrible to implement the operations like above, but I can't really come up with anything else. This works, it does the job but at the cost of a code quality. What could I improve here? I thought of defining operations in Scalar/Complex types but it also feels rather "dirty". What about Scalar+Complex, where should that be implemented? I feel like, having the exact same code in Scalar and Complex classes is the bigger evil. This becomes an issue when implementing multiplication for Vectors not to mention Matrices. The same code in all 4 classes, but If I put it into the base class, I'm back to my current solution...
-
\$\begingroup\$ Why do you treat scalars as different objects than numbers? Aren't they the same thing? \$\endgroup\$sinecode– sinecode2017年03月22日 12:58:52 +00:00Commented Mar 22, 2017 at 12:58
-
\$\begingroup\$ True, but I can't define operators for the Numbers class without casting it to a concrete implementation. \$\endgroup\$Putty– Putty2017年03月22日 13:08:21 +00:00Commented Mar 22, 2017 at 13:08
-
\$\begingroup\$ I think you could treat them as Double. Have you ever heard about composite pattern? If not, I could make you an example. \$\endgroup\$sinecode– sinecode2017年03月22日 13:17:07 +00:00Commented Mar 22, 2017 at 13:17
-
\$\begingroup\$ I know composite pattern but I have no idea how it would eliminate code duplication: Scalar+Complex<>Comper+Scalar. So with the Operations I think I'd still be stuck with my current approach, which is yet again seems horrible but at least it's horrible in only a single file :D. \$\endgroup\$Putty– Putty2017年03月22日 13:19:43 +00:00Commented Mar 22, 2017 at 13:19
-
\$\begingroup\$ I believe that if you extend the Number class you'll always found stuck, that's because it hasn't methods for the imaginary part. In my opinion you should make a single class for Scalars and Complex, integers and real numbers simply have imaginary unit equal to 0. \$\endgroup\$sinecode– sinecode2017年03月22日 14:10:54 +00:00Commented Mar 22, 2017 at 14:10
2 Answers 2
Your problem is fun! Here are a few remarks:
Drop Java's Number class
As @Checkinator said, this class will be limiting to you, because it cannot extend
a higher-level class.
You are going to have to work outside Java's number framework, but you may hook back in with a <? extends Number> asNumber();
method (for printing etc.)
Complex extends Number
This is backwards:
public class Complex extends Number {
Yes, mathematically the complex number extends the real number to the 2D plane. However, this extends
is Java class relationship, and it works the other way around. You need Real extends Complex
because:
Real
s are a subset (a mathematical restriction!) ofComplex
Real
extends the functionality ofComplex
(by adding a total ordering, etc.)
So if we were to use inheritance, we'd inherit the other way around.
> What about Scalar+Complex
This problem is now solved: Scalar + Complex is a Complex
operation (where the Scalar is Enmedded in the Complex plane - because it is a Complex). Don't sweat it, defer that to Complex.add()
.
This is the key issue in your code!
Inheritance is annoying
Inheritance forces you to reuse everything from the mother class. This imposes restrictions in how you design the mother class:
- If I put a
double realPart
anddouble imaginaryPart
inComplex
, all numbers will be able to access an imaginary part. I'll have to ensure everywhere that part is well-constrained. - Worse, the real part will have to be stored as a
double
. Even forInteger
, orRational
subclasses. I would have prefered using oneint
and twoint
s, respectively!
What you really want to pass down from mother class to subclass, is functionalities, not necessarily implementations. Interfaces do that perfectly well, as they inherit each other without bringing an annoying implementation with them:
interface Complex {
Real getRealPart();
Real getImaginaryPart();
Complex add(Complex other);
Complex multiply(Complex other);
}
interface Real extends Complex, Comparable<Real>{
Real add(Real other);
Real multiply(Real other);
}
interface MyInteger extends Real{
MyInteger add(MyInteger other);
MyInteger multiply(MyInteger other);
MyInteger modulo(MyInteger mod);
}
Note I added elementary "natural" operations on those objects, while you introduced an Operator Object. It's purely for quality of life and both can co-exist.
But how do I reuse my code without inheritance, you may ask?
Right, when you use interfaces and do not subclass, you lose the implementations. But you don't have to do it all over again! Here comes @Checkinator's other proposal: the Composite Pattern.
So a Complex implementation could be composed of two Real
s:
public class XYComplex implements Complex {
Real realPart;
Real imaginaryPart;
// Implementations of the interface's methods
}
One nice thing is that you could switch between representations of a complex (I proposed real/img, but you could go R.e^i.Theta etc.) and use Strategy pattern to alternate between representations.
Though to be honest, I'd favor an immutable implementation, where the same Complex number had one or several immutable representations at the same time.
Concerning your Operator
class, you could consider applying your Binary Operators in two passes with a partial application (think functional languages). This will prevent having to check all combinations of type of each operands.
With this scheme, you have more control over operator application:
public interface BinaryOperator<T, U, R> {
PartialApplication<T, U, R> applyPartially(T t);
}
public interface UnaryOperator<T, R> {
R apply(T t);
}
public interface PartialApplication<T, U, R> implements UnaryOperator<U, R> {
public T getFirstArgument();
}
In addition, the intermediate PartialApplication might be Operator-specific and be first-member-specific.
Adding an Integer to something else:
public static class PartialIntegerAddition<U, R> implements PartialApplication<MyInteger, U, R>{
private MyInteger t;
public PartialIntegerAddition(MyInteger t) {
this.t = t;
}
@Override
public R apply(U u) {
if(MyInteger.class.isInstance(u)) {
MyInteger uInteger = (MyInteger) u;
return (R) uInteger.add(t); // natural MyInteger addition
}
if(MyReal.class.isInstance(u)) {
MyReal uReal = (MyReal) u;
return (R) uReal.add(t); // natural MyReal addition
}
if(MyComplex.class.isInstance(u)) {
MyComplex uComplex = (MyComplex) u;
return (R) uComplex.add(t); // natural MyComplex addition
}
throw new ArithmeticException("Unable to perform the operation");
}
@Override
public MyInteger getFirstArgument() {
return t;
}
}
Adding a Real to something else:
public static class PartialRealAddition<U, R> implements PartialApplication<MyReal, U, R>{
private MyReal t;
public PartialRealAddition(MyReal t) {
this.t = t;
}
@Override
public R apply(U u) {
if(MyReal.class.isInstance(u)) {
MyReal uReal = (MyReal) u;
return (R) uReal.add(t); // natural MyReal addition
}
if(MyComplex.class.isInstance(u)) {
MyComplex uComplex = (MyComplex) u;
return (R) uComplex.add(t); // natural MyComplex addition
}
throw new ArithmeticException("Unable to perform the operation");
}
@Override
public MyReal getFirstArgument() {
return t;
}
}
Adding a Complex to something else:
public static class PartialComplexAddition<U, R> implements PartialApplication<MyComplex, U, R>{
private MyComplex t;
public PartialComplexAddition(MyComplex t) {
this.t = t;
}
@Override
@SuppressWarnings("unchecked")
public R apply(U u) {
if(MyComplex.class.isInstance(u)) {
MyComplex uComplex = (MyComplex) u;
return (R) uComplex.add(t); // natural MyComplex addition
}
throw new ArithmeticException("Unable to perform the operation");
}
@Override
public MyComplex getFirstArgument() {
return t;
}
}
Now these 3 implementations can be brought together using this generic Addition Operator (which works with Integers, Reals, complexes, and can be expanded for any super or sub-classes!):
public static class GeneralAddition {
@SuppressWarnings("unchecked")
public <T, U, R> R apply(T t, U u) {
PartialApplication<T, U, R> partialApplication;
if(MyInteger.class.isInstance(t)) {
MyInteger tInteger = (MyInteger) t;
partialApplication = (PartialApplication<T, U, R>) new PartialIntegerAddition<U, R>(tInteger);
} else if(MyReal.class.isInstance(t)) {
MyReal tReal = (MyReal) t;
partialApplication = (PartialApplication<T, U, R>) new PartialRealAddition<U, R>(tReal);
} else if(MyComplex.class.isInstance(t)) {
MyComplex tComplex = (MyComplex) t;
partialApplication = (PartialApplication<T, U, R>) new PartialComplexAddition<U, R>(tComplex);
} else {
throw new ArithmeticException("Unable to perform the operation");
}
return partialApplication.apply(u);
}
}
Example usage:
We've see that we can perform any operation on any combination of Number types, and always get the most specific subtype.
public static void main(String[] argc){
MyInteger a = new MyIntegerImpl(5);
MyReal b = new MyRealImpl(2.5);
GeneralAddition addition = new GeneralAddition();
MyReal result = addition.apply(a, b); // Note: return type is imposed by programmer. May fail (ClassCast) if incoherent. The real type will be the lowest common denominator of the input types.
}
Note: The return type is set by the Programmer. If it is wrong (too specific), it will fail with a ClassCastException. Java cannot infer the correct type automatically because it is illegal to impose a bound on a generic type, base on other generic types:
public <R super U & V, U, V> R add(T t, U u); // Java error
Its true type will still be MyReal
, though, so it is mathematically correct.
Otherwise you can simplify it and lock that R
return type to the highest-level Object (Complex
?) like you did using Number
.
Wrap up
- We have many implementations of each Number types
- The GenericAddition selects the proper PartialOperator for left hand side (1 check)
- The PartialApplication implementations make the minimum number of checks (1 each)