/object-algebra

An implementation of Object Algebras for JavaScript

Primary LanguageTypeScriptGNU Affero General Public License v3.0AGPL-3.0

Object Algebra

Build npm version Downloads

Implementation of Object Algebras to enable Feature-Oriented Programming (FOP).

Installation

The latest version:

npm install @mlhaufe/object-algebra

A specific version:

npm install @mlhaufe/object-algebra@x.x.x

For direct use in a browser (no build step):

<script type="importmap">
{
  "imports": {
    "@mlhaufe/object-algebra": "https://unpkg.com/@mlhaufe/object-algebra/index.mjs",
  }
}
</script>
<script type="module">
  import {Merge} from '@mlhaufe/object-algebra';

  console.log(typeof Merge); // 'function'
</script>

Usage

Declare the Algebra:

interface PointAlg<T> extends Algebra {
    Point2(x: number, y: number): T
    Point3(x: number, y: number, z: number): T
}

Define Data:

class PointData { }
class Point2 extends PointData {
    constructor(readonly x: number, readonly y: number) { super() }
}
class Point3 extends PointData {
    constructor(readonly x: number, readonly y: number, readonly z: number) { super() }
}

Define Factory for the data:

class PointDataFactory implements PointAlg<PointData> {
    Point2(x: number, y: number) {
        return new Point2(x, y)
    }
    Point3(x: number, y: number, z: number) {
        return new Point3(x, y, z)
    }
}

Define a couple traits:

interface IPrintable { print(): string }
class Printable implements PointAlg<IPrintable> {
    Point2(x: number, y: number): IPrintable {
        return {
            print() { return `(${x}, ${y})` }
        }
    }
    Point3(x: number, y: number, z: number): IPrintable {
        return {
            print() { return `(${x}, ${y}, ${z})` }
        }
    }
}

interface IAddable { add(other: PointData & IAddable): this }
class Addable implements PointAlg<IAddable & PointData> {
    Point2(x: number, y: number): IAddable & Point2 {
        const family = this
        return {
            add(other: Point2 & IAddable) { return family.Point2(x + other.x, y + other.y) }
        } as any
    }
    Point3(x: number, y: number, z: number): IAddable & Point3 {
        const family = this
        return {
            add(other: Point3 & IAddable) { return family.Point3(x + other.x, y + other.y, z + other.z) }
        } as any
    }
}

Compose the features into a single class:

import {Merge} from '@mlhaufe/object-algebra';

class PointFactory extends Merge(PointDataFactory, Printable, Addable) { }

// Alternatively:
// const PointFactory = Merge(PointDataFactory, Printable, Addable)

const { Point2, Point3 } = new PointFactory()

const p1 = Point2(1, 2)
const p2 = Point2(3, 4)

console.log(p1.print()) // (1, 2)
console.log(p2.print()) // (3, 4)

console.log(p1.add(p2).print()) // '(4, 6)'

More examples are available in the tests directory.

Future Work

TypeScript does not support Higher Kinded Types (#1213). This means that the Merge function will not track or merge the generics types of the composed classes. This is generally a problem for container types like List. You can see an example of in ListAlg.test.mts directory.

TypeScript also does not support associated types (#17588) so an emulation of HKTs are also not possible via that feature (As described by Bertrand Meyer). Index types are close, but seem to be forgotten by the compiler.

There is another approach to HKTs that does leverage indexed types to some limited success, but it adds an additional syntactic burden to the user which I find unacceptable.

References and Further Reading