Fork me on GitHub
Source file: 3d-vectors.fut

Three-dimensional vectors

This example shows how we might define a module type for programming with vectors in normal 3D space.

module type vspace_3d = {

We keep the type of vector components abstract, because it will be useful to have the same interface for vectors with components that are f32, f64, or something even more exotic.

  type scalar

However, the vector type itself is opaque. It must be a record with x/y/z fields.

  type vector = {x: scalar, y: scalar, z: scalar}

Vectors must support the usual binary operations.

  val +: vector -> vector -> vector
  val -: vector -> vector -> vector
  val *: vector -> vector -> vector
  val /: vector -> vector -> vector

As well as dot products and cross products.

  val dot: vector -> vector -> scalar
  val cross: vector -> vector -> vector

Vectors can be scaled by a scalar.

  val scale: scalar -> vector -> vector

We can also take the length of a vector, as well as normalise a vector to have unit length.

  val length: vector -> scalar
  val normalise: vector -> vector

Finally, vectors can be rotated around the three axes.

  val rot_x : (radians: scalar) -> vector -> vector
  val rot_y : (radians: scalar) -> vector -> vector
  val rot_z : (radians: scalar) -> vector -> vector

}

So for which scalars can this module type be implemented? We describe this with yet another module type.

module type scalar = {
  type t

  val +: t -> t -> t
  val -: t -> t -> t
  val *: t -> t -> t
  val /: t -> t -> t

  val f64: f64 -> t
  val sqrt : t -> t
  val cos : t -> t
  val sin : t -> t
}

Because of Futhark’s structural type system, the built-in modules f32 and f64 already satisfy this interface.

We can now define a parametric module for constructing vector spaces:

module mk_vspace_3d(P: scalar): vspace_3d with scalar = P.t = {
  type scalar = P.t

  type vector = {x: scalar, y: scalar, z: scalar}

  def zero = {x = P.f64 0, y = P.f64 0, z = P.f64 0}
  def one  = P.f64 1

Most of the definitions are straight out of a textbook, and so we won’t be providing much commentary.

We start out by defining two helper functions for doing operations on the components of a vector.

  def map (f: scalar -> scalar) (v : vector) =
    {x = f v.x, y = f v.y, z = f v.z}

  def map2 (f: scalar -> scalar -> scalar) (a : vector) (b : vector) =
    {x = f a.x b.x, y = f a.y b.y, z = f a.z b.z}

This allows us to conveniently define vector arithmetic and scaling.

  def (+) = map2 (P.+)
  def (-) = map2 (P.-)
  def (*) = map2 (P.*)
  def (/) = map2 (P./)
  def scale (s: scalar) = map (s P.*)

The remaining operations are defined explicitly.

  def dot (a: vector) (b: vector) =
    P.(a.x*b.x + a.y*b.y + a.z*b.z)

  def cross ({x=ax,y=ay,z=az}: vector)
            ({x=bx,y=by,z=bz}: vector): vector =
    P.({x=ay*bz-az*by, y=az*bx-ax*bz, z=ax*by-ay*bx})

  def length v = P.sqrt (dot v v)

  def normalise (v: vector): vector =
    let l = length v
    in scale (one P./ l) v

  def rot_x (theta: scalar) ({x,y,z} : vector) =
    let cos_theta = P.cos theta
    let sin_theta = P.sin theta
    in { x
       , y = P.(cos_theta * y - sin_theta * z)
       , z = P.(sin_theta * y + cos_theta * z)}

  def rot_y (theta: scalar) ({x,y,z} : vector) =
    let cos_theta = P.cos theta
    let sin_theta = P.sin theta
    in { x = P.(cos_theta * x - sin_theta * z)
       , y
       , z = P.(sin_theta * x + cos_theta * z)}

  def rot_z (theta: scalar) ({x,y,z} : vector) =
    let cos_theta = P.cos theta
    let sin_theta = P.sin theta
    in { x = P.(cos_theta * x - sin_theta * y)
       , y = P.(sin_theta * x + cos_theta * y)
       , z}
}

Instantiating the module:

module f64_3d = mk_vspace_3d f64

And trying it out:

> f64_3d.cross {x=1,y=2,z=3} {x=4,y=5,z=6}
{x = -3.0f64, y = 6.0f64, z = -3.0f64}

See also

Dex: Multi-step ray tracer

The vector package.