2013. március 22., péntek

Parallel Hierarchies

Back in the day I mused about how generics can get out of control if you have interrelated types in an API and you use generics to enforce type-safety. The Java type system does not handle that situation gracefully. This time I'll describe a different kind of problem where the language let me down a little bit.

We still have types in an API that will be implemented, but now, rather than type interrelations, the focus will be on type hierarchies. Although I could once again turn to the Node types in GameCracker for illustration, I will mix it up a bit and demonstrate my difficulties using my Matrix library.

The Matrix types

The gimmick of this library is that I wanted a type-rich linear algebra package; apart from the general Matrix type, I wanted a subtype for Vectors (1-column matrices), 3x3 matrices, 3-element vectors etc. These specialized types could define methods appropriate for that particular matrix (for example, 3D vectors have a function called cross product), and, the primary reason, common operations like addition and transposition can be defined in subtypes to return a more specific type. Just in case I'm talking nonsense, here is a hopefully helpful example, only focusing on two types and just a couple of methods for the sake of simplicity:
public interface Matrix {
    /** Returns the number of columns. */
    public int getColumns();

    /** Returns the number of rows. */
    public int getRows();


    /** Returns the (row,col) element. */
    public double get(int row, int col);


    /** Adds this matrix and m, and returns the result. */
    public Matrix plus(Matrix m);
}


public interface Vector extends Matrix {

    /** Returns the number of components. */
    public int getDimension();

    /** Returns the i-th component. */
    public double getCoord(int i);

    /** Adds this vector and v, and returns the result. */
    public Vector plus(Matrix m);

    /** Compute the dot product of this vector and v. */
   
public double dot(Vector v)}
Technically everything could be done with the single Matrix type, but being able to use and work with subtypes help readability a lot when doing math. Readability is really-really important. Working with 3D points is so much easier when you have a List<Vector3> rather than a List<Matrix>, not to mention that the Vector3 type can have very nice getX(), getY(), getZ() functions.

Implementing the Types

There are a number of possible implementations for the Matrix API. The most straightforward is one that uses an array to store the elements. Note that the implementation must provide concrete classes for every type: it must have 2D vectors, 3D vectors, it must have vectors that are neither, and it must have matrices that are not vectors. The parent types are not just there to define common functionality to the subtypes.

public class ArrayMatrix implements Matrix {
    private double[][] data;
    @Override
    public int getColumns() {
        return data[0].length;
    }
    @Override
    public int getRows() {
        return data.length;
    }
    @Override
    public double get(int row, int col) {
        return data[row][col];
    }
    @Override
    public Matrix plus(Matrix m) {
        if (m.getRows()!=getRows() || m.getColumns()!=getColumns())
            throw new IllegalArgumentException("size mismatch");
        Matrix result=MatrixFactory.create(m.getRows(), m.getColumns());
        for (int row=0; row<getRows(); row++)
            for (int col=0; col<getColumns(); col++)
                result.set(row, col, get(row, col)+m.get(row, col));
    }
}
public class ArrayVector extends ArrayMatrix implements Vector {
    @Override
    public int getDimension() {
        return getRows();
    }
    @Override
    public double getCoord(int i) {
        return get(i, 0);
    }
    @Override
    public Vector plus(Matrix m) {
        return (Vector)super.plus(m);
    }
    @Override

    public double dot(Vector v) {
        if (getDimension()!=v.getDimension())
            throw new IllegalArgumentException("size mismatch");
        double result=0;
        for (int i=0; i<getDimension(); i++)
            result+=getCoord(i)*v.getCoord(i);
        return result;
    }


}
A couple of disclaimers before I proceed.
  1. First, this does not exactly reflect what I have in my library in terms of names and such. My goal is not to be faithful to the source material, but to provide a simple example.
  2. Second, I just omit constructors and functions like Matrix.set, hoping that no-one will seriously miss them during this discussion.
  3. Third, about the creation of the matrix instances. As part of the gimmick, I want every matrix that is created through operations throughout the library to have the most specific type. For example, if a matrix has only one column, then it needs to be a Vector, so that the user can cast it to one and take advantage of the subtype. Since the API is public, I have no way of enforcing this, but I have a MatrixFactory class that does take care of this constraint, and everywhere in my library I use this factory whenever I need an instance.
  4. All different kinds of matrix implementations have their own factory because of this, but I wanted general API functions that create new instances (like Matrix.plus) to create one of the default implementation, rather than of the same implementation as the 'creator'. The reason for this is to avoid surprises. Imagine that you get two matrices, add them together, try to modify the result, and you get an exception because the first original matrix was actually an ImmutableMatrix, and its plus method also returned an ImmutableMatrix. That would make using the library rather unpleasant; that's why whenever a matrix needs to be created in the library, the default MatrixFactory is used.
Okay, with all these marginally important stuff out of the way, let me direct your attention to the point. Have a look at the ArrayMatrix.plus function. It doesn't actually have anything in it that is specific to this particular matrix implementation kind. In fact this code works for all imaginable implementations. The same is true for the functions in ArrayVector, and in fact for the whole bunch of omitted functions (multiply, submatrix, dot product, matrix norm etc.) Of course it is possible that some special kind of matrix can provide a more efficient implementation to some of these, but they are always a reasonable default. All functions apart from the core implementation-specific Matrix.get, Matrix.set, Matrix.getColumns, and Matrix.getRows will be shared by most subtypes of Matrix.

API as Abstract Classes


To share code with subtypes, you stick that code into a common supertype. Usually. To do that here, we need the API types to be abstract classes rather than interfaces:

public abstract class Matrix {

    /** Returns the number of columns. */
    public abstract int getColumns();

    /** Returns the number of rows. */
    public abstract int getRows();

    /** Returns the (row,col) element. */
    public abstract double get(int row, int col);

    /** Adds this matrix and m, and returns the result. */
    public Matrix plus(Matrix m) {

        if (m.getRows()!=getRows() || m.getColumns()!=getColumns())
            throw new IllegalArgumentException("size mismatch");
        Matrix result=MatrixFactory.create(m.getRows(), m.getColumns());
        for (int row=0; row<getRows(); row++)
            for (int col=0; col<getColumns(); col++)
                result.set(row, col, get(row, col)+m.get(row, col));
    }
   
    // other non-abstract functions
}

public abstract class Vector extends Matrix {
    @Override
    public int getDimension() {
        return getRows();
    }
    @Override
    public double getCoord(int i) {
        return get(i, 0);
    }
    @Override
    public Vector plus(Matrix m) {
        return (Vector)super.plus(m);
    }
    public double dot(Vector v) {
        if (getDimension()!=v.getDimension())
            throw new IllegalArgumentException("size mismatch");
        double result=0;
        for (int i=0; i<getDimension(); i++)
            result+=getCoord(i)*v.getCoord(i);
        return result;
    }

}
That's all nice and good, ArrayMatrix can inherit the matrix operations from Matrix, ArrayVector can inherit the vector operations from Vector.

public class ArrayMatrix extends Matrix {
    private double[][] data;
    @Override
    public int getColumns() {
        return data[0].length;
    }
    @Override
    public int getRows() {
        return data.length;
    }
    @Override
    public double get(int row, int col) {
        return data[row][col];
    }
   
    // all the other functions we'll just inherit

}
public class ArrayVector extends Vector {
    // aww, we no longer extend ArrayMatrix

    private double[][] data;
    @Override
    public int getColumns() {
        return data[0].length;
    }
    @Override
    public int getRows() {
        return data.length;
    }
    @Override
    public double get(int row, int col) {
        return data[row][col];
    }

    // all the other functions inherited from Vector

}
Now ArrayVector can no longer inherit from ArrayMatrix, since it already has a superclass. So rather than duplicating the 'other' functions among different implementations, we need to duplicate the implementation-specific 'core' functions and data structures among the different types of that implementation. And yes, we could use this opportunity to implement ArrayVector using a simple double[] rather than double[][], but we also had this option in the previous version, and now we don't have the option to share the code.

This looks like a trade, and a quite favourable one too (there are a lot of functions that have default implementations), but I have a very strong counterargument. I want ImmutableVector to be an ImmutableMatrix. The immutable implementation is a bit of a special case in that it comes with the guarantee that their matrix elements will never change. Users of the library utilize this guarantee by using the Immutable* types (in contrast to the other implementations like Array*, which don't even need to be public). To have an ImmutableVector that is not an ImmutableMatrix, it doesn't make any sense.

This counterargument doesn't actually seem that strong as I originally thought, but anyway... We tried to avoid code duplication, but didn't really succeed. In Java, ArrayVector cannot inherit common Vector behaviour from the API while inheriting common ArrayMatrix behaviour as well. No multiple inheritence allowed. I opted to keep the type hierarchy in the implementation (that means the API needs to consist of interfaces), and moved the common functions to static helper methods.

Static Helper Class

Now I have something like this:
public class MatrixOp {
    public static Matrix plus(Matrix m1, Matrix m2) {
        // add the matrices, return the result
    }
    public static double dot(Vector v1, Vector v2) {
        // ...
    }
    // other similar functions implementing the common operations
}


public class ArrayMatrix implements Matrix {
    private double[][] data;
    @Override
    public int getColumns() {
        return data[0].length;
    }
    @Override
    public int getRows() {
        return data.length;
    }
    @Override
    public double get(int row, int col) {
        return data[row][col];
    }
    @Override
    public Matrix plus(Matrix m) {
        return MatrixOp.plus(this, m);
    }
}
public class ArrayVector extends ArrayMatrix implements Vector {
    @Override
    public int getDimension() {
        return getRows();
    }
    @Override
    public double getCoord(int i) {
        return get(i, 0);
    }
    @Override
    public Vector plus(Matrix m) {
        return (Vector)super.plus(m);
    }
    @Override
    public double dot(Vector v) {
        return MatrixOp.dot(this, v);
    }

}
This moves the implementations of the functions to a single point, but the functions themselves still need to be present in the concrete implementation types to do the delegating call. (And when the implementation is a single line, I don't even bother with delegating.) That's still quite a bit of repeating code.

You could propose just removing all the fancy operations from the main types, and making them available to the user as static functions in the helper class. While that makes sense from a design perspective, and it get rids of all code duplication, I still have to say no. Having all the functions on the matrix types themselves is really important to me. In the name of readability.

I am eagerly waiting for default methods, coming in Java 8, that will make the ideal implementation possible.

2013. január 11., péntek

Designing GameCracker - Working Without Nodes

Suddenly I've had an idea. What if the Graph API didn't specify nodes at all? Of course, it is nodes that make up the graph, but are they really necessary for using them? The answer in general is probably yes, but in GameCracker, the interactions with a graph are rather limited. I just might be able to turn nodes, normal nodes, and transformation nodes into implementation detail. That would be glorious! It would make both implementing graphs and using them much simpler.

The idea comes from a handy utility class that I've written called Match. It contains a sequence of valid moves, the positions along this path, and the corresponding nodes in the graph. It had two basic operations:
  1. move: in the current position (the last one in the sequence), make a given move, expanding it in the graph if it didn't exist, and append this move and the new position to the match sequence.
  2. back: undo the last move, update the match to reflect the previous position.
The graph is built and expanded by traversing it using the move operation. The back operation is used to backtrack when we encounter a position where the result is known. Now, how exactly the graph is traversed can be anything from depth-first to externally controlled (like using a chess program to try to select a winning move), but these two operations are pretty much all we need.

Besides the nice abstraction that Match provides, it also takes care of the fact that sometimes there are transformation nodes in the graph, and hides it from the 'user', so the state graph can be explored as if there were no transformation nodes. All in all, introducing this class made everything much cleaner and nicer.

And now my idea is: if I have Match, do I even need the nodes? It seems to me very likely that I don't! The main use case (described above) is handled by the Match operations very comfortably. The big question is if there are any other ways it might be useful to interact with the game graph. If all of these interactions can be solved using Match instead of nodes, then I'm a happy camper.

The only major thing I can think of right now is the 'local search'. The match is at a certain point, and I'm waiting for the chess program to suggest to me which is the winning move, so that I go that way instead of exploring bad moves. While I'm waiting for an answer, I do a search on the graph to see if I can find a good move myself. It can happen, for example, that I'm at a position P, and there is a move M that leads to a position Q which I've already solved. The graph already contains a node with position Q and the result 'White wins', but this position had been reached a different way, and the edge between P and Q is not in the graph yet, so the result of P is still unknown. If I were to ask the graph to add move M, then it would find this Q node, and suddenly P would be solved.

It is very possible that the chess program considers a different move stronger, and would guide me in that direction. It can very well be that this move M is something that a player would never make; but following the 'best' route would involve adding more (possibly much-much more) nodes and positions to the graph unnecessarily. (It is unnecessary with regards to proving the result of P.)

It's like that joke about boiling water. You ask a physicist and a mathematician to boil some water in the tea pot, which you place on the table. The engineer picks up the pot, fills it with water, places it on the stove, and turns the flame on. The mathematician does the same. Next, you put a pot filled with water on the table. The engineer picks it up, places it on the stove, and turns the flame on. The mathematician pours out the water and replaces it onto the table, saying that now we have the original problem that has already been solved. In GameCracker, we want to be the mathematician, to solve positions with minimal work rather than finding 'optimal' solutions, to try to keep the state graph as small as possible.

I might have been sidetracked a bit. Anyway, this local search requires the graph to be able to answer the question: here's this position, what do you know about its result? Fortunately the concept of nodes is not necessary for this operation either. And I cannot think of anything else that would require code outside the graph implementation to be able to refer to individual nodes.

To sum up, I don't see any reason why this change would be a bad idea, but I see quite a number of advantages. I'm quite excited about this! Suddenly all the problems I talked about previously, as well as some other issues with nodes, are mitigated, and with a simpler API. All that was required was to question whether the concept of nodes, such an important part of graphs, was actually necessary to expose.