[Prev][Next][Index][Thread]

Better vector math using dependent types



I finally found the solution to a type theory problem that's been bugging me for years.  It's a twist on the "function parameters are covariant" problem, in the special case of mathematical objects like vector spaces and matrices.
 
Here I will use functions from natural numbers (or an upper-bounded subset of natural numbers) to real numbers to represent vectors.  You can think of such functions as fixed-length arrays of real numbers, or of floating point numbers -- I will be lax with the terminology.
 
-- The Problem --
 
When determining whether one function type (or array type, or vector type) is a subtype of another, type systems treat the function parameter type contravariantly.  Conceptually, this isn't what one wants with mathematical objects like vectors.
 
I will use fixed-length arrays as an example below and assume classic array typing rules.  However, you could see the same issue in C++ by declaring a class vec2d {float x,y;} and a class vec3D: public vec2D {float z;}, and a subclass vec4D containing yet another component.
 
Example of the problem in C-style pseudocode:
 
void Use3DVector(float[3] my3DVector) {
    ...do something here
}
void Example() {
    // Declare a 2D vector and a 4D vector.
    my2DVector b={1,2};
    my4DVector a={1,2,3,4};
 
    // Use the 4D vector. This succeeds because it's typesafe:
    // a 4D vector has all the components of a 3D vector and more.
    Use3DVector(my4DVector);
 
    // Use the 2D vector. This fails because it's not typesafe: a 2D
    // vector doesn't have certain components that a 3D vector has.
    Use3DVector(my2DVector);
}
 
In other words, traditional type systems inherently use these subtyping relationships
 
    .. <: float[4] <: float[3] <: float[2] <: float[1]
 
This is the opposite of how mathematicians look at vector spaces.  They see 1D space as a subspace of 2D space; 2D space as a subspace of 3D space, etc:
 
    float[1] <: float[2] <: float[3] <: ..
 
-- The Solution --
 
Define a new type "zero" which is a subtype of "float".  Type "zero" has only one instance, the value "0".  By definition of subtyping, all zeros are floats, but not all floats are zeros.  This is sound.
 
Since "zero" has only one instance, variables of type "zero" don't need to be stored; they take up no runtime storage.  Since they take up no storage, you can store infinitely many of them in a fixed-sized data structure.  I exploit this to represent vectors as infinite-length arrays consisting of a finite number of reals, followed by an infinite number of zeros.
 
Now, instead of looking at vectors in the Java style (float[0], float[1], etc.), we view them with dependent types, like:
 
    ForAll(i nat).if i<3 then float else zero // a 3d vector
    ForAll(i nat).if i<1 then float else zero // a 1d vector
 
Or concepually, you can think of vectors as the examples below, where "..." stands for "followed by infinitely many zeros":
 
    {float,float,float,zero...}
    {float,zero...}
 
If you define a generic type constructor vec(i) in the style above, you'll find that it's exactly what mathematicians want:
 
    vec(1) <: vec(2) <: vec(3) <: ...
 
You can verify this by looking at the "ForAll" expressions above, verifying that for every possible input value "i", the corresponding subtyping rules hold.  For example: {real,zero...} <: {real,real,zero...} because (looking at the first elements) real<:real, and (second elements) zero<:real, and (subsequent elements) zero<:zero, etc.
 
-- Other Benefits --
 
You can then derive types from constants, i.e. {2,1,0} has the type of a 2D vector because the third component is 0, whose exact type is zero rather than the more general float.
 
You can also type strangely-shaped vector spaces like {float,zero,float,zero...} -- the space of vectors with only X and Z components.
 
In languages supporting dependent types and pattern matching, many mathematical rules automatically fall out of the results.  For example, transforming a vector by a matrix which has one column of zeros yields a vector with the corresponding component set to zero -- not just at runtime, but in the type system as well, as long as the zeros are statically known.
 
-- Summary and Future Work --
 
By changing our representation of vectors from fixed-length arrays of reals, to infinite-sized arrays containing finitely many reals and infinitely many zeros, vectors now obey the subtype=subspace rules mathematicians want, rather than the covariant subtyping of traditional fixed-length arrays and records.
 
I am currently working on extending this approach to deal with the multivectors of Geometric (Clifford) Algebra.  There are many ways of doing this which don't involve any new type-theory constructs, but clearly nothing we can build out of ordinary dependent types will admit ordinary scalars, and the newly-defined vectors here, as elements of any more general multivector type.
 
However, I've found a new construct, sort of a "dimensionality-indexed transparent product" that gives you multivectors compatible with traditional scalars, and the vectors defined here.  Better yet, it admits imaginary numbers, complex numbers, quaternions, and spinors of arbitrary dimensionality as subtypes; and it always knows the more specific derivable type of your result.  This is still somewhat sketchy but I'd be glad to share it if there is any interest.
 
-Tim