Omniscient

Functors for N-dimension Vectors Operations

Published: Last updated:
  • #post
  • #ocaml
  • #maths

Let's look at a small example of using a module functor in OCaml to create useful linear algebra modules in arbitrary dimensions. We'll define the definition of a basic vector and then extend it with vector operations!

Vector

(** Base functions that a 2D Vector must support *)
module type Vector = sig
  type t

  val add : t -> t -> t             (* Add two vectors together *)
  val sub : t -> t -> t             (* Subtract a vector from another *)
  val scalar_mul : float -> t -> t  (* Scale a vector *)
  val dot : t -> t -> float         (* Dot product *)
end

Okay, so I'm pretty sure these are the minimum things a Vector needs to support so first things first let's define a module signature for it.

Actually you need even less if you have component-wise multiplication but it's a little tricky to do performantly in OCaml without going back and forth from a list or array representation so we'll add a bit more functionality to our base Vector signature

VectorOps

So now we can create a module functor

(** Functor that takes a module implementing the Vector signature and returns
    a module that implements most vector space operations *)
module VectorOps (V : Vector) = struct
  include V
  let scalar_div s v = scalar_mul (1. /. s) v
  let neg v = scalar_mul (-1.0) v
  let length_squared v = dot v v
  let length v = sqrt (dot v v)
  let magnitude = length
  let normalize v =
    let len = length v in
    if len > 0.0 then scalar_mul (1.0 /. len) v else v
  let distance u v = sub u v |> length
end

that implements all the other vector operations in terms of the base set. (These are not exhaustive; I've added the ones I'm using thus far but I will no doubt add more over time)

Now we have arbitrary dimension vector operations ^_^

Instantiate

module Vec2Float = struct
  type t = { x : float; y : float }
  let add u v = { x = u.x +. v.x; y = u.y +. v.y }
  let sub u v = { x = u.x -. v.x; y = u.y -. v.y }
  let scalar_mul k v = { x = k *. v.x; y = k *. v.y }
  let dot u v = (u.x *. v.x) +. (u.y *. v.y)
end
module Vec3Float = struct
  type t = { x : float; y : float; z : float }
  let add u v = { x = u.x +. v.x; y = u.y +. v.y; z = u.z +. v.z }
  let sub u v = { x = u.x -. v.x; y = u.y -. v.y; z = u.z -. v.z }
  let scalar_mul k v = { x = k *. v.x; y = k *. v.y; z = k *. v.z }
  let dot u v = (u.x *. v.x) +. (u.y *. v.y) +. (u.z *. v.z)
end

So by defining these concrete modules that implement our basic Vector signature, we can give them to the VectorOps functor to produce our final modules that support the full suite of vector functions.

module Vec2 = VectorOps(Vec2Float)
module Vec3 = VectorOps(Vec3Float)
(*
module Vec3 :
  sig
    type t = Vec3Float.t
    val add : t -> t -> t
    val sub : t -> t -> t
    val scalar_mul : float -> t -> t
    val dot : t -> t -> float
    val scalar_div : float -> t -> t
    val neg : t -> t
    val length_squared : t -> float
    val length : t -> float
    val magnitude : t -> float
    val normalize : t -> t
    val distance : t -> t -> float
  end
) *)

We could even add 1D, 4D, 5D vectors and so on if we wanted!


Thanks for reading,
Omni