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 e414b20..f710c78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,3 +9,4 @@ export * from "./Monad.ts" export * from "./Option.ts" export * from "./Pipe.ts" export * from "./Result.ts" +export * from "./Semigroup.ts"