From b1600b178b14424dd25f3c9410b7c736883a8a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Houllier?= Date: Tue, 30 Jun 2026 17:19:05 +0200 Subject: [PATCH 1/2] feat: add Semigroup algebraic structure Add Semigroup interface and concrete implementations (Sum, Product, Str) with full test coverage including associativity law verification. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Semigroup.test.ts | 65 +++++++++++++++++++++++++++++++++++ src/Semigroup.ts | 79 +++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 1 + 3 files changed, 145 insertions(+) create mode 100644 src/Semigroup.test.ts create mode 100644 src/Semigroup.ts diff --git a/src/Semigroup.test.ts b/src/Semigroup.test.ts new file mode 100644 index 0000000..9b754ec --- /dev/null +++ b/src/Semigroup.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, test } from "bun:test" + +import { Product, Str, Sum } from "./Semigroup.ts" + +describe("Semigroup", () => { + describe("Sum", () => { + test("should add values via concat", () => { + const a = new Sum(3) + const b = new Sum(5) + expect(a.concat(b).value).toBe(8) + }) + + test("should satisfy associativity law", () => { + const a = new Sum(1) + const b = new Sum(2) + const c = new Sum(3) + expect(a.concat(b).concat(c).value).toBe(a.concat(b.concat(c)).value) + }) + + test("should correctly render when apply toString()", () => { + const a = new Sum(42) + expect(a.toString()).toBe("Sum(42)") + }) + }) + + describe("Product", () => { + test("should multiply values via concat", () => { + const a = new Product(3) + const b = new Product(5) + expect(a.concat(b).value).toBe(15) + }) + + test("should satisfy associativity law", () => { + const a = new Product(2) + const b = new Product(3) + const c = new Product(4) + expect(a.concat(b).concat(c).value).toBe(a.concat(b.concat(c)).value) + }) + + test("should correctly render when apply toString()", () => { + const a = new Product(7) + expect(a.toString()).toBe("Product(7)") + }) + }) + + describe("Str", () => { + test("should concatenate strings via concat", () => { + const a = new Str("hello") + const b = new Str(" world") + expect(a.concat(b).value).toBe("hello world") + }) + + test("should satisfy associativity law", () => { + const a = new Str("foo") + const b = new Str("bar") + const c = new Str("baz") + expect(a.concat(b).concat(c).value).toBe(a.concat(b.concat(c)).value) + }) + + test("should correctly render when apply toString()", () => { + const a = new Str("hello") + expect(a.toString()).toBe("Str(hello)") + }) + }) +}) diff --git a/src/Semigroup.ts b/src/Semigroup.ts new file mode 100644 index 0000000..e56c2e6 --- /dev/null +++ b/src/Semigroup.ts @@ -0,0 +1,79 @@ +/** + * A Semigroup is a type with an associative binary operation (`concat`). + * For any values a, b, c: a.concat(b).concat(c) === a.concat(b.concat(c)) + */ +export interface Semigroup { + concat(other: T): T +} + +/** + * A Semigroup under addition. + * Wraps a number and combines by adding values together. + */ +export class Sum implements Semigroup { + readonly #value: number + + constructor(value: number) { + this.#value = value + } + + get value(): number { + return this.#value + } + + concat(other: Sum): Sum { + return new Sum(this.#value + other.value) + } + + toString(): string { + return `Sum(${this.#value})` + } +} + +/** + * A Semigroup under multiplication. + * Wraps a number and combines by multiplying values together. + */ +export class Product implements Semigroup { + readonly #value: number + + constructor(value: number) { + this.#value = value + } + + get value(): number { + return this.#value + } + + concat(other: Product): Product { + return new Product(this.#value * other.value) + } + + toString(): string { + return `Product(${this.#value})` + } +} + +/** + * A Semigroup under string concatenation. + * Wraps a string and combines by concatenating values together. + */ +export class Str implements Semigroup { + readonly #value: string + + constructor(value: string) { + this.#value = value + } + + get value(): string { + return this.#value + } + + concat(other: Str): Str { + return new Str(this.#value + other.value) + } + + toString(): string { + return `Str(${this.#value})` + } +} diff --git a/src/index.ts b/src/index.ts index 8041141..ef1250c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,3 +4,4 @@ export * from "./Mapper.ts" export * from "./Monad.ts" export * from "./Option.ts" export * from "./Result.ts" +export * from "./Semigroup.ts" From a0b64cacb3d20402add7fb68eeab00ea565037ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Houllier?= Date: Tue, 30 Jun 2026 17:20:42 +0200 Subject: [PATCH 2/2] feat: add Monoid algebraic structure Implement the Monoid type which extends Semigroup with an identity element (empty). Includes SumMonoid, ProductMonoid, StrMonoid constructors and a generic concatAll function for folding collections. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Monoid.test.ts | 82 ++++++++++++++++++++++++++++++++++++++++++++++ src/Monoid.ts | 59 +++++++++++++++++++++++++++++++++ src/index.ts | 1 + 3 files changed, 142 insertions(+) create mode 100644 src/Monoid.test.ts create mode 100644 src/Monoid.ts diff --git a/src/Monoid.test.ts b/src/Monoid.test.ts new file mode 100644 index 0000000..b0c0903 --- /dev/null +++ b/src/Monoid.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, test } from "bun:test" + +import { Product, Str, Sum } from "./Semigroup.ts" +import { + ProductMonoid, + StrMonoid, + SumMonoid, + concatAll, +} from "./Monoid.ts" + +describe("Monoid", () => { + describe("SumMonoid", () => { + test("empty() returns Sum(0)", () => { + expect(SumMonoid.empty().value).toBe(0) + }) + + test("satisfies right identity: a.concat(empty) === a", () => { + const a = new Sum(42) + expect(a.concat(SumMonoid.empty()).value).toBe(a.value) + }) + + test("satisfies left identity: empty.concat(a) === a", () => { + const a = new Sum(42) + expect(SumMonoid.empty().concat(a).value).toBe(a.value) + }) + }) + + describe("ProductMonoid", () => { + test("empty() returns Product(1)", () => { + expect(ProductMonoid.empty().value).toBe(1) + }) + + test("satisfies right identity: a.concat(empty) === a", () => { + const a = new Product(7) + expect(a.concat(ProductMonoid.empty()).value).toBe(a.value) + }) + + test("satisfies left identity: empty.concat(a) === a", () => { + const a = new Product(7) + expect(ProductMonoid.empty().concat(a).value).toBe(a.value) + }) + }) + + describe("StrMonoid", () => { + test("empty() returns Str('')", () => { + expect(StrMonoid.empty().value).toBe("") + }) + + test("satisfies right identity: a.concat(empty) === a", () => { + const a = new Str("hello") + expect(a.concat(StrMonoid.empty()).value).toBe(a.value) + }) + + test("satisfies left identity: empty.concat(a) === a", () => { + const a = new Str("hello") + expect(StrMonoid.empty().concat(a).value).toBe(a.value) + }) + }) + + describe("concatAll", () => { + test("folds an array of Sum values", () => { + const values = [new Sum(1), new Sum(2), new Sum(3)] + expect(concatAll(SumMonoid, values).value).toBe(6) + }) + + test("folds an array of Product values", () => { + const values = [new Product(2), new Product(3), new Product(4)] + expect(concatAll(ProductMonoid, values).value).toBe(24) + }) + + test("folds an array of Str values", () => { + const values = [new Str("a"), new Str("b"), new Str("c")] + expect(concatAll(StrMonoid, values).value).toBe("abc") + }) + + test("returns identity for an empty array", () => { + expect(concatAll(SumMonoid, []).value).toBe(0) + expect(concatAll(ProductMonoid, []).value).toBe(1) + expect(concatAll(StrMonoid, []).value).toBe("") + }) + }) +}) diff --git a/src/Monoid.ts b/src/Monoid.ts new file mode 100644 index 0000000..1aa6a86 --- /dev/null +++ b/src/Monoid.ts @@ -0,0 +1,59 @@ +import type { Semigroup } from "./Semigroup.ts" +import { Product, Str, Sum } from "./Semigroup.ts" + +/** + * A Monoid is a Semigroup with an identity element (`empty`). + * For any value a: + * a.concat(empty) === a (right identity) + * empty.concat(a) === a (left identity) + */ +export interface Monoid extends Semigroup {} + +/** + * A constructor-like type that provides the identity element for a Monoid. + * Since TypeScript interfaces cannot enforce static methods, we use a + * separate type to represent the "companion object" of a Monoid. + */ +export type MonoidConstructor = { empty(): T } + +/** + * MonoidConstructor for Sum — identity element is 0. + */ +export const SumMonoid: MonoidConstructor = { + empty(): Sum { + return new Sum(0) + }, +} + +/** + * MonoidConstructor for Product — identity element is 1. + */ +export const ProductMonoid: MonoidConstructor = { + empty(): Product { + return new Product(1) + }, +} + +/** + * MonoidConstructor for Str — identity element is the empty string. + */ +export const StrMonoid: MonoidConstructor = { + empty(): Str { + return new Str("") + }, +} + +/** + * Folds an array of monoidal values into a single value using the + * monoid's identity element as the starting accumulator. + * Returns `monoid.empty()` for an empty array. + */ +export function concatAll>( + monoid: MonoidConstructor, + values: T[], +): T { + return values.reduce( + (acc, value) => acc.concat(value), + monoid.empty(), + ) +} diff --git a/src/index.ts b/src/index.ts index ef1250c..765aca2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,4 @@ export * from "./Monad.ts" export * from "./Option.ts" export * from "./Result.ts" export * from "./Semigroup.ts" +export * from "./Monoid.ts"