Sunday, December 30, 2007

possibile use of java annotations to declare type mapping betwenn different representation of abstract types

As we can find looking back at this textbook (Structure and Interpratations of Computer Programs) and particularly to this chapter, in order to have a sound data type hierarchy there must be a strong relation between different concrete implementation of an abstract data type.

The relation could simply be "the caller, that expects an abstract type, should not be aware of the particular actual type".

Who and how is responsible to make possible that the concrete type does not violates the caller expectations?

In the simple case of an immutable type, it is enough for us that there is a sound mapping attributes that is able to convert an instance of an actual type to the equivalent instance of the other actual type.

Where do we put this mapping?

For any constructor it will be declared how to convert his parameters, to the parameters needed by another constructor, in order to get an equivalent object, no matter if it is the same or of a different actual type.

The mapping is possible using the following mathematical relation between polar and rectangular reresentation of complex numbers:





The miniproject, with some unit tests, is versioned here.

The link between the caller and the actual implementation is provided by a factory, that is able to introspect the annotations to obtain the mapper that will be used to get a particular implementation.

Any new implementation should declare a mapping between different preexisting implementation.

We note that in this example we are using immutable objects.
Would we investigate also to mutable as well, we should be aware that it will not be just a matter of equivalence during object creation, but also after any repeated sequences of step that can change object state.

There is some work to address this issue as well.

Let's see some sketch of the underlining ideas:

Definition of complex:

public interface ComplexNumber {
public double getReal();
@observer public double getImg();
}

Here a Rectangular implementation:

public class RectangularComplexNumber implements ComplexNumber {
protected double real;
protected double img;

@instanceConverter(instanceConverterMap = CartesianToPolarMapper.class)
public RectangularComplexNumber(double real, double img) {
this.real=real;
this.img = img;
}

public double getReal() {
return real;
}

public double getImg() {
return img;
}

...


}



Polar representation has a different constructor, and two extra methods. It mantains also angle and magnitude, not just img and real:

public class ComplexNumberPolar implements ComplexNumber {
protected double magnitude;
protected Angle angle;

protected double real;
protected double img;

@instanceConverter (instanceConverterMap = PolarToCartesianMapper.class)
{
this.magnitude = magnitude;
this.angle = angle;

this.real = magnitude*Math.cos(angle.getValue());
this.img = magnitude*Math.sin(angle.getValue());
}

public double getReal() {
return real;
}

public double getImg() {
return magnitude*Math.sin(angle.getValue());
}


public boolean equals(Object object)
{
...
}

public int hashCode()
{

}
public String toString()
{
}
}

We can see the two attributes associated to each constructor, respectively


@instanceConverter (instanceConverterMap = RectangularToPolarMapper.class)
@instanceConverter (instanceConverterMap = PolarToCartesianMapper.class)

This new attribute is defined in this way, and specify by default an identity mapper:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CONSTRUCTOR)
public @interface instanceConverter {
Class instanceConverterMap() default IdentityToObject.class;
}


We specified different mappers
public class RectangularToPolarMapper implements WrapperToObject{
public Wrapper getWrapperForClassConstructorWithParameters(
Class targetClass,
Class targetParamsTypes[],
Class[] originParamsTypes)
{

if (targetClass.equals(ComplexNumberPolar.class)) {
return new CartesianToPolar();
}
throw new RuntimeException("wrapper not defined for the target class");
}
}

The returned "wrapper", is the following:

public class CartesianToPolar implements Wrapper {
public Object[] wrap(Object[] object) {
return new Object[] {Math.sqrt((Math.pow((Double) object[0],2.0))+Math.pow((Double) object[1],2.0)),new Angle(Math.atan2((Double) object[1], (Double) object[0]))};
}
}


This is the one that is returned to make the inverse transformation:

public class PolarToCartesian implements Wrapper{
public Object[] wrap(Object[] object) {
return new Object[]{(Double)object[0]*
Math.cos(((Angle)object[1]).getValue()),
(Double)object[0]*Math.sin(((Angle)object[1]).getValue())};
}
}


The guy that uses all that stuffs is the factory, that provide a link from abstraction type to actual type:

public class ComplexNumbersFactory {
....
public static ComplexNumber getComplexFromCartesianPar(double first ,double second)
{
if (RECTANGULAR.equals(implementation))
{
return new ComplexNumberCartesian(first,second);
}

if (POLAR.equals(implementation))
{

ComplexNumber converted = (ComplexNumber) Utilities.getInstanceOfThisActualGivenConstructorOfOther(
ComplexNumberPolar.class,ComplexNumberCartesian.class,
new Object[]{first,second},
new Class[]{double.class,double.class},
new Class[]{double.class,Angle.class});
return converted;
}
throw new RuntimeException("unadmitted implementation mode "+implementation);
}

}

He fails if the lookup of the mappers fails, and it is a good thing, because it means that someone extended the concrete implementation without providing a way to relate the concrete implementation to the others.

Just for curiosity, the reflection based mapper lookup is done by the following code:

  public static Object getInstanceOfThisActualGivenConstructorOfOther(
Class targetClass,
Class originClass,
Object[] instanceOriginCompatible,
Class[] instanceOriginClasses,
Class[] instTargetClass)
{
try {
Constructor constructor = originClass.getConstructor(instanceOriginClasses);
WrapperToObject wrapper = (WrapperToObject) constructor.getAnnotation(instanceConverter.class).instanceConverterMap().newInstance();
Object[] convertedPars = ((wrapper.getWrapperForClassConstructorWithParameters(targetClass, instTargetClass,instanceOriginClasses).wrap(instanceOriginCompatible)));
Constructor targetConstructor = targetClass.getConstructor(instTargetClass);
Object convertedObject = targetConstructor.newInstance(convertedPars);
return convertedObject;

} catch (Exception e) {
throw new RuntimeException(e);
}
}

The factory shows a (real,img) way to construct the object, thus it has nothing to do if the concrete implementation is RECTANGULAR, otherwhise it lookups the mapper examining the constructor, and return the Polar concrete type.

At the end, we can show some junit test to verify that the things are ok:
   ComplexNumber first = new ComplexNumberCartesian(1.0,1.0);
// the actual of first is Rectangular
ComplexNumbersFactory.setImplementation(ComplexNumbersFactory.POLAR);

ComplexNumber second= ComplexNumbersFactory.getComplexFromCartesianPar(1.0,1.0);
// the actual of second is Polar.

assertEquals(((ComplexNumberPolar)second).getAngle(),new Angle(Math.PI/4));
assertEquals(((ComplexNumberPolar)second).getMagnitude(),Math.sqrt(2.0));
// no cast exeption

assertEquals(first,second);


Everything works as expected for the number 1+i.


Summary

A concrete type can be more efficient than another, but we need a save way to change concrete types.

Attribute are used to annotate to constructors how they are related to other constructor to guarantee cross types equivalences.

Something more than using only "extends" or "implements" keywords.

The due of plugging attributes to the constructor declaring the mapper is like saying: hi, if you are going to create a new implementation, you should also take care of how to make possible substitution between any other preexisting implementation with yours, and of course we expect that it will not affect any behavior expected from the other implementations.

(just a little note: the code is versioned, and it has also been refactored, soo can be a little bit different from how it has been shown in this post).


Regards, and Happy New Year!

T.

No comments: