For point 1, object algebras do support this even though I didn't show it in the post:
interface ExprAlg<Num, Bool> {
lit: (value: number) => Num;
add: (left: Num, right: Num) => Num;
eq: (left: Num, right: Num) => Bool;
iff: (interrogee: Bool, then: Num, els: Num) => Num;
}
const evaluate: ExprAlg<number, boolean> = {
lit: (value) => value,
add: (left, right) => left + right,
eq: (left, right) => left === right,
iff: (interrogee, then, els) => interrogee ? then : els
}
function makeExample<Num, Bool>(alg: ExprAlg<Num, Bool>): Num {
return alg.iff(
alg.eq(
alg.add(alg.lit(2), alg.lit(2)),
alg.lit(4)),
alg.lit(1),
alg.lit(2))
}
console.log(makeExample(evaluate)); // prints 1
For point 2, you are correct that the original Java formulation of object algebras does not support data sort extensibility. I haven't tried to see if TS's more powerful generics change this or not.
For point 3, consider what happens if you want to add a new variant to the data type. In this case, add a Mult
variant for multiplication. With GADTs this is not possible without modifying or duplicating the existing evaluation code, but I can do it with object algebras:
type ExtendedExprAlg<Num, Bool> = ExprAlg<Num, Bool> & {
mult: (left: Num, right: Num) => Num;
}
const extendedEvaluate: ExtendedExprAlg<number, boolean> = Object.assign({}, evaluate, {
mult: (left: number, right: number) => left * right
})
function makeExtendedExample<Num, Bool>( alg: ExtendedExprAlg<Num, Bool>): Num {
const one = alg.mult(alg.lit(1), alg.lit(1));
return alg.iff(
alg.eq(one, makeExample(alg)),
alg.lit(3),
alg.mult(alg.lit(2), alg.lit(2))
)
}
console.log(makeExtendedExample(extendedEvaluate)); // prints 3
This is the point of object algebras. ADTs and GADTs can't do this out of the box; this is the essence of the expression problem and is why more advanced techniques like final tagless encodings and datatypes a la carte exist.