Thursday, August 20, 2009

Vector: Philosophy

I thought I'd go over some of my design decisions for this project. The first, and most obvious, is that I decided to forgo efficiency in terms of scope. I'm not one of those people who say efficiency is not important; in my experience, when doing serious geometric work, code can never be too fast. But by the same token, it will be years before it might even make sense to think about using Perl 6 for serious numerical work. So it makes good sense here to shoot for versatility rather than speed. If for some reason you think it makes sense to have an 13-dimensional vector of complex numbers, Vector can do it.

I've also chosen to let Perl 6 generate errors like mismatched dimensions rather than catching them directly in code. For instance, I've only defined the cross product for 3D and 7D Vectors. If you try to call cross product on vectors of other dimensions, you will get a "No applicable candidates found to dispatch to" error message. Now that I think about this, I'm inclined to think this is the wrong answer. I could provide more awesome error messages without a whole lot more work. I should probably figure out the right way to do that.

There are two closely related classes that frequently go with a Vector class: UnitVector and Point. In Perl 6, UnitVector is a simple matter of saying subset UnitVector of Vector where { (1 - 1e-10) < $^v.Length < (1 + 1e-10) };. (I discussed the implications of doing it this way in my previous post.)

Point is a little more tricky. An N-dimensional point looks pretty much exactly like an N-dimensional vector. The only practical difference I'm aware of is how they handle being transformed. With a point, you factor in the change to the origin of the change of basis matrix. With a vector, you ignore the change to the origin. (There are other type distinctions, like a point subtracted from another point should be a vector, and it doesn't make sense to add two points. But in practice, these distinctions cause problems without any corresponding pluses that I can see.)

Most of the geometry libraries I've worked with ignore this distinction in the type system entirely, just making points and vectors two different names for the same class. My current inclination is to ignore points altogether, making everything a Vector. I think I will have to try implementing some actual code using Vector to see how this works out.

Another issue I'm pondering is Length and LengthSquared. I actually have multiple issues with these functions as implemented, and I haven't worked out what the proper solution is. As currently structured, they are in the wrong spot in the file. Both are most cleanly implemented in terms of dot product. But they are defined before dot product, because they are Vector class members while dot product is an external operator that works on the Vector public interface.

The obvious answer to this is to make Length and LengthSquared regular subs that take Vectors. But that brings on really weird conundrums of object-orientation. The only reason to have a LengthSquared function is that for positive x & y, x < y iff x*x < y*y, so you can save two square root calculations if all you're interested in is the relative lengths of two vectors. That might make sense if you've got an efficient low-level C++ implementation of a 2D vector. It's kind of silly if you're thinking of a more high-level implementation of, say, a 9D vector.

But worse, it actually amounts to exposing the underlying representation of the Vector! It is "an optimization" because calculating the length involves a square root call. But many sensible alternate representations of Vectors (like polar coordinates) will actually store the length directly -- in which case LengthSquared requires more calculations than Length, rather than fewer!

So, those arguments have convinced me to do away with the LengthSquared function altogether. (Not yet reflected in code as I type this.) On the other hand, they suggest to me that Length really should be a Vector member function, because while for this implementation of Vector it can efficiently be a non-member function, for many other implementations that would represent a serious pessimization. (I was going to say that on the other hand Unitize should be external, but there are cases when that would be a pessimization as well. Hmmm.)

As you've probably guessed by now, writing about these things forces me to think about them more deeply!

General: I've updated to the PDX release of Rakudo this morning, and Vector still passes all 204 tests! Though that number is likely to go down when I delete LengthSquared, because I'll need to remove some tests in the process.

3 comments:

  1. I don't think multi dimension Vector shold have special cases for 3D or 7D vectors. It can however I see more it more useful to have Vector::2D and Vector::3D classes. If my code is all about 2D math then I want have some nice methods, for example x() and y().

    CamelCase considered bad habbit these days and I don't think it's good idea to use it in perl6 code.

    ReplyDelete
  2. If Vector is to provide support for cross product, it has to have the 3D and 7D special cases -- they are the only cases cross product is defined for.

    I think a real case can be made for making Vector a base class (or perhaps even better, a role?) with Vector::2DFloat, Vector::3DFloat, and Vector::Anything subclasses. But at this point I'd rather forge on and do something with Vector rather than polishing Vector until it is perfect.

    Though I do think I should define a shorter way to get at a Vector's coordinate values ASAP. I can't believe I didn't think of that right from the start, actually. It certainly has the potential to drastically clean up the cross product functions, and demonstrate another Perl 6 operator trick at the same time...

    ReplyDelete
  3. Getting a better error message for cross products on dimensions other than 3 and 7 should not be hard.

    I suppose that the cross product is implemented either as a sub or a method (or an operator, which is either of them), in which case you can use simple multi dispatch: Add a general (Vector, Vector) multi that dies with a nice error message, and more specific multis with signatures like (Vector $x where { $x.dimension == 3}, Vector $y where { $y.dimension == 3 }).

    ReplyDelete