Minborg

Minborg
Minborg

Friday, December 23, 2016

Day 23, Java Holiday Calendar 2016, Use Mappable Types Instead of Bloated Ones

Day 23, Java Holiday Calendar 2016, Use Mappable Types Instead of Bloated Ones



Today's tips is about mappable types. Traditionally we Java developer have relied on inheritance and types with a number of methods to support convenient use of our classes. A traditional Point class would contain x and y coordinates and perhaps also a number of functions that allows us to easily work with our Point.

Imagine now that we add new shapes like Circle and that Circle inherits from Point and adds a radius. Further imagine that there are a bunch of other geometric shapes like Line, Square and the likes that also appear in the code. After a while, all our classes becomes entangled in each other and we end up in a messy hair ball.

However, there is another way of structuring our geometric classes such that there is a minimum of inter-class coupling. Instead of letting each geometric class provide specific methods for translations like add() and flipAroundXAxis() we could add just one or two generic methods that operates on the geometric figure and returns a value of any type. We could then break out the old methods from the geometric classes and convert them to functions and just apply them on the objects rather than letting the objects handle that responsibility.

Let us take the concept for a spin!

Traits

We start with some basic Traits of the geometrical shapes that are shared amongst most shapes. Here are the basic traits:

interface HasX { int x(); } // x is the x-coordinate

interface HasY { int y(); } // y is the y-coordinate

interface HasR { int r(); } // r is the radius

What's the Point?

Now it is time to create our Point interface like this:

interface Point extends HasX, HasY {

    static Point point(int x, int y) {
        return new Point() {
            @Override public int x() { return x; }
            @Override public int y() { return y; }
        };
    }

    static Point origo() { return point(0, 0); }

    default <R> R map(Function<? super Point, ? extends R> mapper) {
        return Objects.requireNonNull(mapper).apply(this);
    }

    default <R, U> R map(BiFunction<? super Point, ? super U, ? extends R> mapper, U other) {
        return Objects.requireNonNull(mapper).apply(this, other);
    }

}

I have purposely refrained from creating an implementation class of the Point interface. Instead, each time the static point() method is called, an instance from an anonymous class is created. This illustrates that the implementation class is "pure" and does not inherit or override anything. In a real solution, there could of course be an implementation of an immutable class PointImpl.

In the middle, there is a conveniency method that return a point in origo (e.g. point(0, 0)).

The two methods at the end of the class are where the interesting stuff starts. They allow us to apply almost any function to a Point.

The first one takes a mapping function that, in turn, takes a point and maps it so something else (i.e. a Function)

The last one takes a mapping function that takes two points and maps it to a Point (e.g. a Binary Function). The mapping function applies the current Point (i.e. "this") as the first parameter and then it also applies another point. That other point is given as the second argument to the map() function. Complicated? Not really. Read more and it will be apparent what is going on.

The Functions

Now we can define a number of useful functions that we could apply to the Point class.

interface PointFunctions {

    static UnaryOperator<Point> NEGATE = (f) -> point(-f.x(), -f.y());
    static BinaryOperator<Point> ADD = (f, s) -> point(f.x() + s.x(), f.y() + s.y());
    static BinaryOperator<Point> SUBTRACT = (f, s) -> point(f.x() - s.x(), f.y() - s.y());
    static UnaryOperator<Point> SWAP_OVER_X_AXIS = (f) -> point(f.x(), -f.y());
    static UnaryOperator<Point> SWAP_OVER_Y_AXIS = (f) -> point(-f.x(), f.y());
    static BinaryOperator<Point> BETWEEN = (f, s) -> point((f.x() + s.x()) / 2, (f.y() + f.y()) / 2);
    static Function<Point, String> TO_STRING = (p) -> String.format("(%d, %d)", p.x(), p.y());
    static BiFunction<Point, Point, Boolean> EQUALS = (f, s) -> (f.x() == s.x()) && (f.y() == s.y());
    //
    static BiFunction<Point, Point, Double> DISTANCE = (f, s) -> Math.sqrt((f.x()-s.x())^2+(f.y()-s.y())^2);
    //
    static Function<Point, UnaryOperator<Point>> ADD2 = s -> (f) -> point(f.x() + s.x(), f.y() + s.y());
}


These methods (or any other similar methods or lambdas) can now be applied to points without polluting the classes.

Example of Usage

This code snippet will create a first point at origo, apply a number of translations to it and then convert it to a string using the TO_STRING mapper. Note how easy it would be to use another string mapper because now the "to string" functionality is separate from the class itself. It would also be easy to use a custom lambda instead of one of the pre-defined functions.

System.out.println(
     origo()
         .map(ADD, point(1, 1))       // 1
         .map(SWAP_OVER_X_AXIS)       // 2
         .map(NEGATE)                 // 3
         .map(BETWEEN, point(-1, -1)) // 4
         .map(TO_STRING)
    );

This will produce the following output:

(-1, 0)

As can be seen in the picture below, this seams to be correct:




Expanding the Concept 

If we later introduce a Circle interface like this

interface Circle extends HasX, HasY, HasR {
  // Stuff similar to Point...
}

and we change our methods so that they can work with the traits directly (and after some additional refactoring not shown here) we can get a decoupled environment where the functions can be made to operate on any relevant shape. For example, the following method can return a "toString" mapper that can work on anything having an x and a y value (e.g. both Point and Circle)

static <T extends HasX & HasY> Function<T, String> toStringXY() {
    return (T t) -> String.format("(%d, %d)", t.x(), t.y());
}

After further modification, we could take a Circle and ADD a Point to it and the circle will be translated using the point's coordinates.

Alternate Solutions

It is possible to wrap any class in an Optional and then use the Optional's map() method to map values. In that case we do not need the map() functions in the shapes. On the other hand, we would have to create an Optional and then also get() the end value once all mappings have been applied.

Another way would be just having a bunch of static methods that takes a Point and other shapes as parameters like this:

static <T extends HasX & HasY> Point add(Point p, T other) {
    return point(p.x() + other.x(), p.y() + other.y());
}

But that... is another story...

Follow the Java Holiday Calendar 2016 with small tips and tricks all the way through the winter holiday season. I am contributing to open-source Speedment, a stream based ORM tool and runtime. Please check it out on GitHub.

2 comments:

  1. return Objects.requireNonNull(mapper).apply(this);

    I think, 'Objects.requireNonNull(' here is redundant.

    ReplyDelete
    Replies
    1. True. I always make a habit of checking input parameters. In this case though, the result would be the same regardless of the check...

      Delete