This gist contains some experiments/ideas for typing geometric-algebra code in TypeScript.
These ideas might be applied to PEGEAL at some point.
| /* | |
| Branding of vector spaces | |
| ------------------------- | |
| Each vector belongs to a vector space. A vector space should operate on its | |
| own vectors only. We can check this at run time, but we can also use branded | |
| types to separate vector spaces already at compile time, at least to some | |
| degree. | |
| (That same idea is of course also applicable to geometric algebras.) | |
| */ | |
| { // Run-time checks only | |
| class Vector { | |
| constructor( | |
| readonly owner: VectorSpace, | |
| readonly x: number, | |
| readonly y: number, | |
| ) {} | |
| } | |
| class VectorSpace { | |
| vector(x: number, y: number) { return new Vector(this, x, y); } | |
| checkMine(v: Vector) { | |
| if (v.owner !== this) throw "foreign vector given"; | |
| } | |
| add(a: Vector, b: Vector) { | |
| this.checkMine(a); | |
| this.checkMine(b); | |
| return this.vector(a.x + b.x, a.y + b.y); | |
| } | |
| } | |
| const vs1 = new VectorSpace(); | |
| const a = vs1.vector(1, 2), b = vs1.vector(3, 4); | |
| const vs2 = new VectorSpace(); | |
| const c = vs2.vector(5, 6); | |
| vs1.add(a, b); // ok | |
| vs1.add(a, c); // throws | |
| } | |
| { // Run-time and compile-time checks | |
| class Vector<Brand> { | |
| constructor( | |
| readonly owner: VectorSpace<Brand>, | |
| readonly x: number, | |
| readonly y: number, | |
| ) {} | |
| } | |
| class VectorSpace<Brand> { | |
| vector(x: number, y: number) { | |
| return new Vector(this, x, y); | |
| } | |
| checkMine(v: Vector<Brand>) { | |
| if (v.owner !== this) throw "foreign vector given"; | |
| } | |
| add(a: Vector<Brand>, b: Vector<Brand>) { | |
| this.checkMine(a); | |
| this.checkMine(b); | |
| return this.vector(a.x + b.x, a.y + b.y); | |
| } | |
| get #brand(): Brand { return undefined as Brand; } | |
| } | |
| // Enable one of the following blocks to define brands for vs1 and vs2: | |
| /*/ | |
| type vs1_brand = "vs1"; | |
| type vs2_brand = "vs2"; | |
| /* | |
| declare const vs1_: unique symbol; type vs1_brand = typeof vs1_; | |
| declare const vs2_: unique symbol; type vs2_brand = typeof vs2_; | |
| /*/ | |
| const enum vs1_brand {} | |
| const enum vs2_brand {} | |
| /**/ | |
| const vs1 = new VectorSpace<vs1_brand>; | |
| const vs2 = new VectorSpace<vs2_brand>; | |
| const a = vs1.vector(1, 2), b = vs1.vector(3, 4), c = vs2.vector(5, 6); | |
| const d: Vector<vs2_brand> = c; | |
| vs1.add(a, b); // ok | |
| // vs1.add(a, c); // doesn't typecheck | |
| // In VS Code place the cursor after "." and type Ctrl-Blank to get suggestions: | |
| a.x | |
| } |
This gist contains some experiments/ideas for typing geometric-algebra code in TypeScript.
These ideas might be applied to PEGEAL at some point.
| /* | |
| TypeScript Experiments for Geometric Algebra | |
| ============================================ | |
| Linear Algebra | |
| -------------- | |
| In a vector space with a set C of coordinates (for example {"x", "y", "z"}) | |
| a vector maps each member of C to a real number. | |
| These real numbers may have TypeScript type `number`, but we also allow | |
| different types (to support partial evaluation). So we parameterize vectors | |
| and other types by some type T, which should be a generalization of `number`. | |
| A vector for coordinates {"x", "y", "z"} might have the TypeScript type | |
| {x: T, y: T, z: T} | |
| Geometric Algebra | |
| ----------------- | |
| In geometric algebra (a.k.a. Clifford algebra) we use "multivectors". | |
| A multivector maps each subset of the coordinate set C to a real number. | |
| So with our coordinates {"x", "y", "z"} we have components for the 2**3 = 8 | |
| subsets | |
| {}, {"x"}, {"y"}, {"x", "y"}, {"z"}, {"x", "z"}, {"y", "z"}, {"x", "y", "z"}. | |
| A multivector for our coordinates might have this TypeScript type: | |
| {"1": T, x: T, y: T, x_y: T, z: T, x_z: T, y_z: T, x_y_z: T} | |
| Using Tuples | |
| ------------ | |
| Alternatively we might address components numerically, which leads to vectors | |
| of type | |
| [T, T, T] | |
| and multivectors of type | |
| [T, T, T, T, T, T, T, T] | |
| We need some standard assignment of components to array positions. | |
| But this is easy: Just represent a subset S ⊆ C as an integer `i` interpreted | |
| as a bitmap where the bits represent the presence of "x", "y" and "z" in S, | |
| starting from the least significant bit. Then `i` is also the corresponding | |
| index for the multivector 8-tuple. | |
| Potential Usage | |
| --------------- | |
| Actually for the implementation of multivector operations arrays with computed | |
| indices are more natural than tuples and objects. But: | |
| - Object and tuple types might make sense in higher-level APIs for specialized | |
| geometric algebras such as projective and conformal GAs. | |
| - The techniques explored here might also be used to declare stricter types to | |
| the functions in | |
| https://github.com/hcschuetz/pegeal/blob/main/src/componentNaming.ts. | |
| - And another idea: PEGEAL could easily emit JS code. To use that emitted code | |
| from hand-written code we can wrap it in JS `Function`s. Could the types of | |
| such functions be expressed with the types introduced here? (Analogous | |
| considerations hold for calling PEGEAL-generated WASM code from JS.) | |
| */ | |
| { console.log("Addressing components by name (object) ======================="); | |
| console.log("To be used in examples: --------------------------------------"); | |
| const xy = Object.freeze(["x", "y"] as const); | |
| type XY = typeof xy; | |
| console.log(xy); | |
| const xyz = Object.freeze(["x", "y", "z"] as const); | |
| type XYZ = typeof xyz; | |
| console.log(xyz); | |
| console.log("Vector-level definitions: ------------------------------------"); | |
| /** **R**ead-**O**nly **S**tring **A**rray */ | |
| type ROSA = readonly string[]; | |
| type Vector<Keys extends ROSA, T> = Record<Keys[number], T>; | |
| const complete = | |
| <C extends ROSA, T>(defaults: Vector<C, 0>) => | |
| (partial: Partial<Vector<C, T>>): Vector<C, T | 0> => | |
| Object.assign({}, defaults, partial); | |
| const zeroVector = <C extends ROSA>(c: C) => | |
| Object.freeze(c.reduce((acc, k) => Object.assign(acc, {[k]: 0}), {})) as Vector<C, 0>; | |
| console.log("Vector-level examples:----------------------------------------"); | |
| const zeroVector_XYZ = zeroVector(xyz); | |
| console.log(zeroVector_XYZ) | |
| const completeXYZ = complete(zeroVector_XYZ) as | |
| <T>(partial: Partial<Vector<XYZ, T>>) => Vector<XYZ, T>; | |
| let v: Partial<Vector<XYZ, number | string>> = {x: 2, y: "foo"}; | |
| console.log(completeXYZ(v)); | |
| console.log("Multivector-level type definitions: --------------------------"); | |
| type ExtendString<T extends string, U extends string> = `${T}_${U}`; | |
| type MapExtendString<A extends ROSA, S extends string> = | |
| { [K in keyof A]: ExtendString<A[K], S> } | |
| ; | |
| type PowRaw<Names extends ROSA> = | |
| Names extends readonly [...infer Rest extends ROSA, infer Last extends string] | |
| ? readonly [...PowRaw<Rest>, ...MapExtendString<PowRaw<Rest>, Last>] | |
| : readonly ["1"] | |
| ; | |
| type Simplify<T extends string> = | |
| T extends `1_${infer U extends string}` ? U : T | |
| ; | |
| type MapSimplify<Raw extends ROSA> = { [K in keyof Raw]: Simplify<Raw[K]> }; | |
| type Pow<Names extends ROSA> = MapSimplify<PowRaw<Names>>; | |
| let a: Pow<XYZ>; | |
| type MultiVector<Keys extends ROSA, T> = Vector<Pow<Keys>, T>; | |
| console.log("Multivector-level function definitions: ----------------------"); | |
| function powRaw(keys: ROSA): ROSA { | |
| if (keys.length === 0) return ["1"]; | |
| const [first, ...rest] = keys; | |
| const rec = pow(rest); | |
| return rec.flatMap(r => [r, `${first}_${r}`]); | |
| } | |
| const pow = <Keys extends ROSA>(keys: Keys) => | |
| powRaw(keys).map(s => s.replace(/_1$/, "")) as Pow<Keys>; | |
| const zeroMV = <C extends ROSA>(c: C) => | |
| zeroVector(pow(c)) as MultiVector<C, 0>; | |
| console.log("Examples: ----------------------------------------------------"); | |
| // Shorthands for particular type parameters: | |
| const zeroMV_XYZ = zeroMV(xyz); | |
| console.log(zeroMV_XYZ); | |
| type MV3 = MultiVector<XYZ, number | string>; | |
| const completeMV3 = complete(zeroMV_XYZ) as (partial: Partial<MV3>) => MV3; | |
| const mv3 = completeMV3({"1": 4, x_y: 6.28, x_z: "foo", y_z: "bar"}); | |
| console.log(mv3); | |
| // Some other type parameters: | |
| const zeroMV_XY = zeroMV(xy); | |
| console.log(zeroMV_XY); | |
| type MV2 = MultiVector<XY, number>; | |
| const completeMV2 = complete(zeroMV_XY) as (partialMV: Partial<MV2>) => MV2; | |
| const mv2 = completeMV2({"1": 4, x_y: 6.28}); | |
| console.log(mv2); | |
| // We can even "lift" mv2 to type MV3: | |
| console.log(completeMV3(mv2)); | |
| // ... or project | |
| const powXY = pow(xy) as Pow<XY>; | |
| const project3to2 = (from: MV3) => | |
| powXY.reduce( | |
| (to, key) => { to[key] = from[key]; return to; }, | |
| {} as MultiVector<XY, number | string>, | |
| ); | |
| console.log(project3to2(mv3)); | |
| } | |
| { console.log('"Disjunctive" implementation of Pow: ========================='); | |
| type ROSA = readonly string[]; | |
| type PowRaw<A extends ROSA> = | |
| A extends [...infer Rest extends ROSA, infer Last extends string] | |
| ? PowRaw<Rest> | `${PowRaw<Rest>}_${Last}` | |
| : "1" | |
| ; | |
| type Simplify<T extends string> = T extends `1_${infer U}` ? U : T; | |
| type Pow<A extends ROSA> = Simplify<PowRaw<A>> | |
| type MultiVector<A extends ROSA, T> = Record<Pow<A>, T>; | |
| let b: Partial<MultiVector<["x", "y", "z"], number | symbol>> = { | |
| "1": 5, | |
| x_y: Symbol("foo"), | |
| y_z: 9 | |
| }; | |
| } | |
| { console.log("Addressing components numerically (array/tuple): ============="); | |
| // See https://2ality.com/2025/01/typescript-tuples.html#utility-type-Repeat | |
| type CreateTuple<Len extends number, T, Acc extends Array<unknown> = []> = | |
| Acc['length'] extends Len | |
| ? Acc | |
| : CreateTuple<Len, T, [...Acc, T]> | |
| ; | |
| type Duplicate<A extends Array<unknown>> = [...A, ...A]; | |
| type MVAux<T, Tuple> = | |
| Tuple extends readonly [unknown, ...infer Rest] | |
| ? Duplicate<MVAux<T, Rest>> | |
| : [T] | |
| ; | |
| type MultiVector<T, N extends number> = MVAux<T, CreateTuple<N, unknown>>; | |
| let a: MultiVector<number | string, 3>; | |
| let b: MVAux<number | string, ["x", "y", "z"]>; | |
| } |