diff --git a/CHANGELOG.md b/CHANGELOG.md index 321dfdf..b834226 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ All notable changes to the project are documented in this file. +## 0.3.0 — Typeclass Laws and Property-Based Testing + +### Added + +- Reusable test-scope `EqLaws`, `OrdLaws`, `SemigroupLaws`, and `MonoidLaws`. +- jqwik 1.10.1 for property-based testing on the JUnit Platform. +- Property suites for Integer, String, LocalDate, List, and Person instances. +- Custom jqwik generators for dates, immutable lists, and `Person`. +- `UnlawfulInstancesTest`, demonstrating that integer subtraction implements the Java shape of a semigroup but violates associativity. +- `docs/LAWS.md` explaining executable algebraic contracts and property-based verification. +- Detailed milestone 0.3 implementation task list. + +### Changed + +- Project version advanced from `0.2.0` to `0.3.0`. +- `PersonInstances` names now include their typeclass family: `SHOW_COMPACT`, `SHOW_VERBOSE`, `EQ_ALL_FIELDS`, `EQ_NAME`, `ORD_AGE`, and `ORD_NAME`. +- Existing examples and unit tests were updated for the normalized instance names. +- README, architecture, algebra, typeclass documentation and roadmap now describe executable laws. +- `Semigroup` Javadoc no longer describes law verification as future work. + +### Removed + +- Ambiguous pre-0.3 `PersonInstances` constant names. No deprecated aliases were retained because the API is still pre-1.0. +- Stale milestone-0.2 documentation describing property-based law verification as future work. +- No generated build output, IDE state, obsolete scripts or unused compatibility shims are included. + +### Design notes + +- Law helpers remain under `src/test` and therefore do not expand the production API. +- Laws compare algebraic results using explicit `Eq` instances rather than `Objects.equals`. +- Focused unit tests are retained alongside property tests; the two test styles serve complementary purposes. +- jqwik is test-only and does not leak into production dependencies. +- ADTs, HKT, `Functor`, `Applicative`, `Monad`, registries and automatic derivation remain outside milestone 0.3. + ## 0.2.0 — Basic Algebraic Typeclasses ### Added @@ -41,7 +75,7 @@ All notable changes to the project are documented in this file. - Domain types remain unaware of their typeclass instances. - Instance selection remains explicit; no registry, reflection or automatic resolution was introduced. - `Ord` refines `Eq` and `Monoid` refines `Semigroup`. -- Algebraic laws are documented but systematic property-based verification is intentionally deferred to milestone 0.3. +- Algebraic laws were documented in 0.2 and are executable from milestone 0.3 onward. - HKT, ADTs, `Functor`, `Applicative` and `Monad` remain out of scope for YAGNI reasons. ## 0.1.1 — Foundation Hardening diff --git a/README.md b/README.md index 7959502..41b75d5 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,29 @@ # Java Typeclasses -An educational implementation of **typeclass-oriented programming in modern Java**. +Educational implementation of typeclass-oriented functional programming in modern Java. -Milestone **0.2** introduces the first algebraic typeclasses on top of the hardened 0.1.1 foundation. The project deliberately keeps typeclass instances as explicit values and uses explicit dictionary passing so that the mechanism remains visible. +Milestone **0.3.0** builds on the algebra introduced in 0.2 and adds **executable typeclass laws** verified with property-based tests. -## Requirements +## Goals + +The project demonstrates how typeclass-style polymorphism can be expressed in Java while keeping domain models independent from behavior policies. It deliberately favors explicit mechanics over framework magic. + +Core design principles: -- Java 21+ -- Gradle 9.8.0 (wrapper configuration is provided) +- **SOLID** — small abstractions with focused responsibilities; +- **KISS** — explicit instance passing instead of hidden resolution; +- **DRY** — reusable law helpers remove duplicated test logic; +- **YAGNI** — no HKT encoding, registry, reflection, annotations, `Functor` or `Monad` yet; +- **immutability by default** — examples and collection instances do not mutate caller-owned data. + +## Requirements -The CI matrix verifies Java 21, 25 and 27. +- Java 21 or newer; +- Gradle 9.8.0 when regenerating/running the wrapper; +- JUnit 6.1.2; +- jqwik 1.10.1 for property-based testing. -## What milestone 0.2 contains +## Typeclasses implemented ```text Show @@ -27,63 +39,77 @@ Semigroup Monoid ``` -- `Show` — rendering behavior introduced in 0.1.1. -- `Eq` — selectable equality semantics. -- `Ord` — total ordering that also provides equality. -- `Semigroup` — associative binary combination. -- `Monoid` — a semigroup with an identity element. +`Show` is independent. `Ord` refines `Eq`; `Monoid` refines `Semigroup`. + +## Why laws matter + +Implementing an interface is not enough to create a valid algebraic instance. A `Semigroup` must be associative, and a `Monoid` must additionally have a left and right identity. Likewise, equality and ordering instances must satisfy their corresponding laws. + +Milestone 0.3 verifies these properties systematically using reusable law functions and jqwik-generated values. + +Examples: -This milestone intentionally does **not** add HKT encoding, `Functor`, `Applicative`, `Monad`, reflection-based lookup, a global registry, annotations, or automatic derivation. +```text +Eq: + reflexivity + symmetry + transitivity + +Ord: + reflexivity + sign antisymmetry + transitivity + equality consistency + +Semigroup: + associativity + +Monoid: + associativity + left identity + right identity +``` + +See [`docs/LAWS.md`](docs/LAWS.md) for details. -## Core idea +## Explicit instances -A domain type does not implement its typeclass: +Domain types do not implement their typeclasses: ```java public record Person(String name, int age) { } ``` -Instead, behaviour exists independently: +Instead, behavior is external: ```java -public static final Eq BY_NAME = - (left, right) -> left.name().equalsIgnoreCase(right.name()); +public static final Eq EQ_ALL_FIELDS = + (left, right) -> left.name().equals(right.name()) + && left.age() == right.age(); -public static final Ord BY_AGE = - (left, right) -> Integer.compare(left.age(), right.age()); +public static final Eq EQ_NAME = + (left, right) -> left.name().equalsIgnoreCase(right.name()); ``` -The same domain value can therefore have several meaningful behaviors without modifying `Person`. +This allows several valid interpretations for the same domain type. -## Multiple instances for one type +## Multiple lawful monoids for the same Java type -`Integer` demonstrates why typeclass instances are values rather than behavior embedded in the data type: +`Integer` has two useful monoids: ```java IntegerInstances.ADDITION IntegerInstances.MULTIPLICATION ``` -Both are lawful `Monoid` instances but have different operations and identities: - -```text -addition combine = + empty = 0 -multiplication combine = * empty = 1 -``` - -A generic algorithm can select behavior explicitly: +The same algorithm can therefore produce different results solely through the supplied instance: ```java var numbers = List.of(1, 2, 3, 4); -var sum = MonoidFunctions.combineAll( - numbers, - IntegerInstances.ADDITION); - -var product = MonoidFunctions.combineAll( - numbers, - IntegerInstances.MULTIPLICATION); +var sum = MonoidFunctions.combineAll(numbers, IntegerInstances.ADDITION); +var product = MonoidFunctions.combineAll(numbers, IntegerInstances.MULTIPLICATION); ``` Results: @@ -93,109 +119,128 @@ Results: 24 ``` -## Semigroup versus Monoid +## Property-based law verification -A `Semigroup` only knows how to combine two values. Consequently `SemigroupFunctions.combineAll` requires a non-empty iterable. +A reusable law is independent from any specific instance: -A `Monoid` additionally defines `empty()`, so `MonoidFunctions.combineAll` also works for an empty iterable and returns the identity value. +```java +public static boolean associative( + A first, + A second, + A third, + Semigroup semigroup, + Eq eq) { -## Package structure + var left = semigroup.combine(semigroup.combine(first, second), third); + var right = semigroup.combine(first, semigroup.combine(second, third)); -```text -io.codeswarm.typeclasses -├── core -│ ├── Show -│ ├── Eq -│ └── Ord -├── algebra -│ ├── Semigroup -│ └── Monoid -├── syntax -│ ├── ShowFunctions -│ ├── EqFunctions -│ ├── OrdFunctions -│ ├── SemigroupFunctions -│ └── MonoidFunctions -├── instances -│ ├── IntegerInstances -│ ├── StringInstances -│ ├── LocalDateInstances -│ └── ListInstances -└── examples - ├── Person - ├── PersonInstances - ├── ShowExample - ├── EqExample - ├── OrdExample - ├── SemigroupExample - └── MonoidExample + return eq.eqv(left, right); +} ``` -## Design principles +jqwik then generates many values: -### SOLID +```java +@Property +void additionIsALawfulMonoid( + @ForAll int first, + @ForAll int second, + @ForAll int third) { + + assertTrue(MonoidLaws.associative( + first, + second, + third, + IntegerInstances.ADDITION, + IntegerInstances.EQ)); +} +``` -- **SRP:** typeclasses define capabilities, instances define concrete semantics, syntax classes contain generic algorithms, domain records contain domain state and validation. -- **OCP:** new instances can be added without modifying domain types or generic algorithms. -- **LSP:** `Ord` is a valid `Eq` and `Monoid` is a valid `Semigroup`. -- **ISP:** each typeclass exposes only the minimal operation necessary for its abstraction. -- **DIP:** generic algorithms depend on `Show`, `Eq`, `Ord`, `Semigroup`, or `Monoid`, not on concrete implementations. +The tests also contain an intentionally unlawful subtraction semigroup to demonstrate that merely satisfying the Java interface is not sufficient. -### KISS / DRY / YAGNI +## Packages + +```text +src/main/java/io/codeswarm/typeclasses/ +├── algebra/ +│ ├── Monoid.java +│ └── Semigroup.java +├── core/ +│ ├── Eq.java +│ ├── Ord.java +│ └── Show.java +├── examples/ +│ ├── Person.java +│ ├── PersonInstances.java +│ └── ... +├── instances/ +│ ├── IntegerInstances.java +│ ├── ListInstances.java +│ ├── LocalDateInstances.java +│ └── StringInstances.java +└── syntax/ + ├── EqFunctions.java + ├── MonoidFunctions.java + ├── OrdFunctions.java + ├── SemigroupFunctions.java + └── ShowFunctions.java + +src/test/java/io/codeswarm/typeclasses/ +├── laws/ +│ ├── EqLaws.java +│ ├── MonoidLaws.java +│ ├── OrdLaws.java +│ ├── SemigroupLaws.java +│ └── UnlawfulInstancesTest.java +├── properties/ +│ ├── IntegerInstancesProperties.java +│ ├── ListInstancesProperties.java +│ ├── LocalDateInstancesProperties.java +│ ├── PersonInstancesProperties.java +│ └── StringInstancesProperties.java +└── ... existing focused unit tests +``` -- explicit instance passing instead of a hidden runtime registry; -- no reflection or dependency injection; -- no HKT encoding before it is required; -- no `Functor`/`Monad` before ADTs and laws are established; -- each utility class covers one typeclass family; -- reusable generic folds avoid duplicating combination logic. +The law framework intentionally lives under `src/test`. It validates this project but is not yet committed as public production API. ## Build -With a generated Gradle wrapper: - ```bash ./gradlew clean check javadoc ``` -If the binary wrapper JAR is not present after extracting the source archive, regenerate it once with a local Gradle installation: - -```bash -gradle wrapper --gradle-version 9.8.0 -``` +`check` runs both normal JUnit tests and jqwik properties through the JUnit Platform. -## Examples +## CI -After compiling, run the example classes: +GitHub Actions validates Java 21, 25 and 27 with: -```text -ShowExample -EqExample -OrdExample -SemigroupExample -MonoidExample +```bash +./gradlew clean check javadoc ``` ## Documentation -- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — architecture and design decisions. -- [`docs/TYPECLASSES.md`](docs/TYPECLASSES.md) — typeclass pattern explained. -- [`docs/ALGEBRA.md`](docs/ALGEBRA.md) — algebraic structures introduced in 0.2. -- [`docs/IMPLEMENTATION_TASKS.md`](docs/IMPLEMENTATION_TASKS.md) — implementation sequence for 0.2. -- [`docs/ROADMAP.md`](docs/ROADMAP.md) — planned evolution toward laws, ADTs, HKT and higher abstractions. -- [`CHANGELOG.md`](CHANGELOG.md) — version history. +- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) +- [`docs/TYPECLASSES.md`](docs/TYPECLASSES.md) +- [`docs/ALGEBRA.md`](docs/ALGEBRA.md) +- [`docs/LAWS.md`](docs/LAWS.md) +- [`docs/ROADMAP.md`](docs/ROADMAP.md) +- [`CHANGELOG.md`](CHANGELOG.md) + +## Out of scope for 0.3 -## Next milestone +The following are deliberately deferred: -Milestone **0.3** will focus on typeclass laws and property-based testing: +- `Option`, `Either`, `Validated`; +- higher-kinded type encoding; +- `Functor`, `Applicative`, `Monad`; +- instance registry / `summon`; +- reflection-based resolution; +- annotation processing and automatic derivation. -- `Eq` laws; -- `Ord` laws; -- `Semigroup` associativity; -- `Monoid` associativity and identity; -- reusable law-test harness; -- jqwik property tests. +These belong to later milestones after the basic algebra has a trustworthy law-tested foundation. ## License -Apache License 2.0. See [`LICENSE`](LICENSE). +Apache License 2.0. diff --git a/build.gradle b/build.gradle index b9bf813..e32f727 100644 --- a/build.gradle +++ b/build.gradle @@ -4,9 +4,9 @@ plugins { } group = 'io.codeswarm' -version = '0.2.0' +version = '0.3.0' -description = 'Educational implementation of typeclass-oriented programming and basic algebraic typeclasses in modern Java.' +description = 'Educational implementation of typeclass-oriented programming, algebraic typeclasses, and executable algebraic laws in modern Java.' java { toolchain { @@ -23,6 +23,7 @@ repositories { dependencies { testImplementation platform('org.junit:junit-bom:6.1.2') testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation 'net.jqwik:jqwik:1.10.1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/docs/ALGEBRA.md b/docs/ALGEBRA.md index 5124532..1fbf81c 100644 --- a/docs/ALGEBRA.md +++ b/docs/ALGEBRA.md @@ -1,10 +1,8 @@ -# Basic Algebra — Milestone 0.2 - -Milestone 0.2 introduces two algebraic structures: `Semigroup` and `Monoid`. +# Basic Algebra — Milestone 0.3 ## Semigroup -A semigroup is a type together with an associative binary operation: +A semigroup is a type plus an associative binary operation: ```text combine(combine(a, b), c) @@ -12,7 +10,7 @@ combine(combine(a, b), c) combine(a, combine(b, c)) ``` -The interface is intentionally minimal: +The Java representation remains deliberately minimal: ```java @FunctionalInterface @@ -21,49 +19,28 @@ public interface Semigroup { } ``` -Because a semigroup has no identity value, a generic fold cannot produce a lawful result for an empty collection. `SemigroupFunctions.combineAll` therefore rejects an empty iterable. +Milestone 0.3 verifies associativity with generated values rather than relying only on documentation or a few examples. ## Monoid -A monoid extends a semigroup with an identity element `empty()`. - -In addition to associativity, it must satisfy: +A monoid is a semigroup with an identity element: ```text combine(empty(), a) == a combine(a, empty()) == a ``` -This makes an empty fold meaningful. - -## Multiple monoids over the same type - -There is no unique monoid for `Integer`. - -### Addition - -```text -combine(a, b) = a + b -empty() = 0 -``` - -### Multiplication - -```text -combine(a, b) = a * b -empty() = 1 -``` - -This is a central reason for representing algebraic behaviour as external typeclass instances. - -## String concatenation +Examples in the project: -Strings form a monoid under concatenation with `""` as the identity. +- integer addition with identity `0`; +- integer multiplication with identity `1`; +- string concatenation with identity `""`; +- list concatenation with identity `[]`. -## List concatenation +## Equality used by laws -Lists form a monoid under concatenation with the empty list as the identity. The milestone implementation avoids mutating source lists and returns an immutable result. +Law checking does not hard-code `Object.equals`. The law receives an `Eq` instance, allowing algebraic results to be compared according to the same explicit typeclass philosophy as the rest of the project. -## Law testing +## Laws as executable contracts -Unit tests in 0.2 verify API behaviour, not algebraic correctness over the entire input domain. Milestone 0.3 will add reusable law definitions and property-based tests for associativity, identities, equality laws and ordering laws. +The interfaces remain tiny. Their semantic contracts are verified in test scope through `SemigroupLaws` and `MonoidLaws` plus jqwik property suites. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 76c23d8..534129c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,103 +1,114 @@ -# Architecture — Milestone 0.2 +# Architecture — Milestone 0.3 -## Purpose +## Overview -Milestone 0.2 evolves the 0.1.1 foundation into a small algebraic typeclass library while deliberately retaining explicit, understandable mechanics. - -## Layers +Milestone 0.3 preserves the small production architecture from 0.2 and adds a separate verification layer in test scope. ```text -Domain values -(Person, Integer, String, LocalDate, List) - │ - │ described by external instances - ▼ -Typeclasses -┌──────────────────────────────┐ -│ Show │ -│ Eq ◄── Ord │ -│ Semigroup ◄── Monoid │ -└──────────────┬───────────────┘ - │ implemented by - ▼ -Instances -(IntegerInstances, StringInstances, - LocalDateInstances, ListInstances, - PersonInstances) - │ consumed by - ▼ -Generic syntax/algorithms -(ShowFunctions, EqFunctions, - OrdFunctions, SemigroupFunctions, - MonoidFunctions) + Domain types + │ + │ described by + ▼ + ┌─────────────────────┐ + │ Typeclasses │ + │ │ + │ Show │ + │ Eq <- Ord │ + │ Semigroup │ + │ ^ │ + │ │ │ + │ Monoid │ + └─────────┬───────────┘ + │ implemented as explicit values + ▼ + Instances + │ + │ exercised by + ┌─────────▼───────────┐ + │ Syntax / algorithms │ + └─────────────────────┘ + +Test verification layer: + +Instances + generated values + │ + ▼ + reusable Laws + │ + ▼ + jqwik properties ``` -## Package responsibilities +## Production packages ### `core` -Contains small capability interfaces that are not specifically combination algebras: `Show`, `Eq`, and `Ord`. +Defines non-algebraic/general typeclasses: `Show`, `Eq`, and `Ord`. ### `algebra` -Contains algebraic combination structures: `Semigroup` and `Monoid`. - -### `syntax` - -Contains reusable algorithms that consume typeclass instances explicitly. A separate class exists for each family to avoid a growing generic utility class. +Defines `Semigroup` and `Monoid`. ### `instances` -Contains reusable instances for JDK types and generic instance factories such as list concatenation. +Contains reusable instances for JDK types. ### `examples` -Contains the `Person` domain type, its domain-specific instances, and executable examples. +Contains the `Person` record, its external instances and executable examples. -## Type relationships +### `syntax` -`Ord` extends `Eq` because a total ordering determines equality through `compare(a, b) == 0`. +Contains small generic algorithms grouped by typeclass family. This avoids a growing utility god class. -`Monoid` extends `Semigroup` because every monoid provides the associative `combine` operation and additionally supplies an identity value. +## Test packages -`Show` is intentionally independent from those hierarchies. +### `laws` -## Explicit dictionary passing +Contains reusable executable laws. They are deliberately test infrastructure rather than production API. -A generic algorithm receives its behaviour as an argument: +### `properties` -```java -MonoidFunctions.combineAll(values, IntegerInstances.ADDITION); -``` +Contains jqwik property suites applying the laws to concrete instances. -No hidden global lookup occurs. This keeps dependencies local, testable, and visible. +### existing focused unit-test packages -## Multiple instances +Retain example-based tests for direct behavior, edge cases and readability. -A Java class must not be forced to choose one globally privileged behaviour. For example, both integer addition and multiplication form monoids, while `Person` can be ordered by age or by name. +## Dependency direction -This is why instances remain external values rather than interfaces implemented by domain classes. +Production code never depends on test laws or jqwik: -## Immutability boundary +```text +core/algebra <- instances/examples/syntax -The list concatenation monoid does not mutate either input. It returns an immutable copy. This avoids surprising aliasing effects and makes the algebraic operation easier to reason about. +production code + ▲ + │ tested by +laws + properties + JUnit tests +``` -## SOLID +## SOLID / KISS / DRY / YAGNI -- **SRP:** interfaces, instances, algorithms, and domain state have separate responsibilities. -- **OCP:** add instances and generic consumers without changing domain classes. -- **LSP:** subtype relationships reflect actual capability refinement. -- **ISP:** each typeclass exposes the smallest meaningful interface. -- **DIP:** generic algorithms depend on abstractions supplied by callers. +- **SRP:** each typeclass, syntax helper, law family and property suite has one purpose. +- **OCP:** new instances can be added without modifying domain models or generic algorithms. +- **DIP:** generic algorithms and laws depend on abstractions such as `Eq` and `Semigroup`. +- **KISS:** instance passing remains explicit; no resolver or reflection is introduced. +- **DRY:** algebraic laws are encoded once and reused by many property suites. +- **YAGNI:** law helpers remain test-only until there is a demonstrated need to publish them. -## KISS, DRY, YAGNI +## Naming rule for instance constants -The milestone intentionally excludes automatic instance discovery, reflection, annotations, DI frameworks, HKT encoding, and advanced FP abstractions. Those features would obscure the core concepts or introduce requirements that do not yet exist. +When one holder exposes several typeclass families, names include the family: -## Compatibility decision from 0.1.1 +```text +SHOW_COMPACT +EQ_NAME +ORD_AGE +``` -`TypeClassFunctions` was removed rather than retained as a deprecated facade. The project is still pre-1.0 and has no stable compatibility promise. Keeping it would duplicate the new syntax API and violate the cleanup goal. +This scales better than ambiguous names such as `BY_NAME`. -## Deferred design questions +## Null policy -Milestone 0.3 will address executable algebraic laws. HKT representation, ADTs, instance resolution and derivation are intentionally deferred to later roadmap stages. +The project still does not impose one global null policy. Instances are tested with their intended non-null domains. Null-handling semantics may be introduced later only if there is a concrete use case. diff --git a/docs/IMPLEMENTATION_TASKS.md b/docs/IMPLEMENTATION_TASKS.md deleted file mode 100644 index 072e3fd..0000000 --- a/docs/IMPLEMENTATION_TASKS.md +++ /dev/null @@ -1,131 +0,0 @@ -# Milestone 0.2 — Detailed Implementation Tasks - -## Goal - -Turn the hardened 0.1.1 foundation into the first useful algebraic typeclass release without introducing higher-kinded types or hidden instance resolution. - -## Task 1 — Freeze and verify the 0.1.1 baseline - -1. Preserve the 0.1.1 public concepts: `Show`, external instances, explicit dictionary passing, `Person` as an independent domain record. -2. Remove generated build output before starting. -3. Use the 0.1.1 source tree as the only migration source; do not restore obsolete scripts or experimental files from pre-hardening revisions. - -## Task 2 — Version and build metadata - -1. Change project version from `0.1.1` to `0.2.0`. -2. Keep Java 21 as the minimum toolchain. -3. Keep JUnit, JaCoCo, source JAR and Javadoc JAR configuration. -4. Keep strict compiler warnings with `-Xlint:all`. -5. Keep CI verification on Java 21, 25 and 27. - -## Task 3 — Introduce `Eq` - -1. Add `core/Eq.java`. -2. Keep the interface functional with only `eqv` as the abstract operation. -3. Add `neqv` as a derived default method. -4. Document that equality is selectable and independent from `Object.equals`. -5. Add focused unit tests. - -## Task 4 — Introduce `Ord` - -1. Add `core/Ord.java` extending `Eq`. -2. Add abstract `compare`. -3. Derive `eqv`, `lessThan`, `greaterThan`, `min`, and `max` from `compare`. -4. Avoid extending `Comparator` in this milestone to keep the typeclass model independent and minimal. -5. Add unit tests for all derived operations. - -## Task 5 — Introduce the algebra package - -1. Add `algebra/Semigroup.java` with only `combine`. -2. Document associativity as a law rather than trying to encode it in the Java type system. -3. Add `algebra/Monoid.java` extending `Semigroup` with `empty`. -4. Document left and right identity laws. -5. Add representative unit tests. - -## Task 6 — Refactor generic helper functions - -1. Remove `core/TypeClassFunctions.java`. -2. Add `syntax/ShowFunctions.java`. -3. Add `syntax/EqFunctions.java`. -4. Add `syntax/OrdFunctions.java`. -5. Add `syntax/SemigroupFunctions.java`. -6. Add `syntax/MonoidFunctions.java`. -7. Give each utility class exactly one typeclass-family responsibility. -8. Validate required strategy/instance arguments at API boundaries. -9. Make semigroup folding reject empty input. -10. Make monoid folding use `empty()` for empty input. - -## Task 7 — Expand standard instances - -### Integer - -Add `SHOW`, `EQ`, `ORD`, `ADDITION`, and `MULTIPLICATION`. - -### String - -Add `SHOW`, `EQ`, `LEXICOGRAPHIC_ORD`, `CASE_INSENSITIVE_ORD`, and `CONCATENATION`. - -### LocalDate - -Keep `ISO_SHOW` and add `EQ` and chronological `ORD`. - -### List - -Add a generic `concatenation()` monoid factory. Do not mutate input lists; return an immutable copy. - -## Task 8 — Expand the Person example - -1. Keep `Person` free of typeclass interfaces. -2. Keep `COMPACT_SHOW` and `VERBOSE_SHOW`. -3. Add `BY_ALL_FIELDS` and case-insensitive `BY_NAME` equality instances. -4. Add `BY_AGE` and `BY_NAME_ORDER` orderings. -5. Test that different policies produce different valid results. - -## Task 9 — Add executable examples - -1. Migrate `ShowExample` to `ShowFunctions`. -2. Add `EqExample` showing two equality policies. -3. Add `OrdExample` showing age and name ordering. -4. Add `SemigroupExample` showing a non-empty maximum fold. -5. Add `MonoidExample` showing sum and product from the same `List`. - -## Task 10 — Unit-test the complete milestone API - -1. Test `Eq` and `Ord` default methods. -2. Test representative `Semigroup` and `Monoid` behaviour. -3. Test all syntax helpers and exceptional cases. -4. Test every standard instance family. -5. Test `Person` validation and all domain instances. -6. Do not add fake example-based “law proofs”; reserve property law testing for 0.3. - -## Task 11 — Javadoc - -1. Document every public type. -2. Document every public method and public instance field where the intent is not self-evident. -3. Explain algebraic laws in the relevant interfaces. -4. Explain the design reason behind external instances and explicit dictionary passing. - -## Task 12 — Documentation - -1. Rewrite README for milestone 0.2. -2. Update `ARCHITECTURE.md` with type relationships and package boundaries. -3. Expand `TYPECLASSES.md` with equality, ordering and algebra. -4. Add `ALGEBRA.md`. -5. Update `ROADMAP.md` and mark 0.2 as current. -6. Add a complete 0.2.0 entry to `CHANGELOG.md`. - -## Task 13 — Cleanup - -1. Delete `TypeClassFunctions` and its old test. -2. Delete old aggregated standard-instance tests replaced by focused per-family tests. -3. Do not include obsolete shell scripts or generated `build`, `.gradle`, IDE, coverage or class files. -4. Keep only wrapper configuration that belongs to the source distribution. - -## Task 14 — Verification - -1. Compile production sources with Java 21 and `-Xlint:all`. -2. Compile and run all unit tests through Gradle when the wrapper is available. -3. Generate Javadoc with doclint. -4. Run all executable examples and compare output with README documentation. -5. Verify no stale references to version 0.1.1 remain except historical documentation and changelog. -6. Package the clean source tree as milestone 0.2. diff --git a/docs/LAWS.md b/docs/LAWS.md new file mode 100644 index 0000000..e0cb9ff --- /dev/null +++ b/docs/LAWS.md @@ -0,0 +1,96 @@ +# Typeclass Laws — Milestone 0.3 + +## Why laws are necessary + +A Java interface describes available operations, but it cannot express the mathematical properties those operations should obey. Therefore a class or lambda may compile while still being an invalid typeclass instance. + +Milestone 0.3 turns the documented algebraic contracts into executable test rules. + +## Eq laws + +For an `Eq` instance: + +### Reflexivity + +```text +a ~ a +``` + +### Symmetry + +```text +a ~ b <=> b ~ a +``` + +### Transitivity + +```text +a ~ b && b ~ c => a ~ c +``` + +## Ord laws + +`Ord` refines `Eq` through `compare(a, b) == 0`. + +The test suite verifies: + +- self comparison yields zero; +- reversing arguments reverses the sign of comparison; +- the less-than-or-equal relation is transitive; +- `eqv(a, b)` agrees with zero comparison. + +An `Ord` instance may intentionally consider different domain values equivalent. For example, ordering `Person` only by age means two people of equal age compare as zero. + +## Semigroup law + +A `Semigroup` must be associative: + +```text +(a <> b) <> c == a <> (b <> c) +``` + +The two results are compared using an explicit `Eq` instance rather than `Objects.equals`. This preserves the project's typeclass semantics. + +## Monoid laws + +A `Monoid` inherits semigroup associativity and adds an identity element `empty()`: + +```text +empty <> a == a +a <> empty == a +``` + +## Why property-based testing + +Example-based unit tests can demonstrate known cases but cannot provide meaningful confidence about universal algebraic rules. Property-based testing generates many inputs and shrinks failing cases to smaller counterexamples. + +The project uses jqwik through the JUnit Platform. + +## Law helpers + +Reusable helpers are located in test scope: + +```text +src/test/java/io/codeswarm/typeclasses/laws/ +``` + +They return booleans and do not depend on an assertion library. This keeps them reusable by both property tests and focused example-based tests. + +## Lawful and unlawful instances + +Integer addition is associative and has identity `0`, so it is a lawful monoid. + +Integer subtraction is not associative: + +```text +(10 - 3) - 2 = 5 +10 - (3 - 2) = 9 +``` + +`UnlawfulInstancesTest` intentionally demonstrates this failure. The subtraction operation can implement `Semigroup` syntactically, but it does not satisfy the semigroup law semantically. + +## Why laws remain in test scope + +Milestone 0.3 uses laws as internal verification infrastructure. Publishing them as production API would create a compatibility commitment before the project has enough experience with their shape. + +A future milestone may extract them into a dedicated module if users need reusable law suites. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1aa4058..3d17e59 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -6,32 +6,20 @@ Initial experiment proving that the typeclass pattern can be represented in Java ## 0.1.1 — Foundation hardening -- modern build baseline; -- `Show`; -- explicit dictionary passing; -- external instances; -- clean package boundaries; -- tests, CI and documentation. - -## 0.2 — Basic algebraic typeclasses — current - -- `Eq`; -- `Ord`; -- `Semigroup`; -- `Monoid`; -- multiple instances for the same type; -- immutable list concatenation; -- dedicated syntax helpers; -- algebra documentation. - -## 0.3 — Typeclass laws +Modern build baseline, `Show`, explicit dictionary passing, external instances, tests, CI and documentation. -Planned: +## 0.2 — Basic algebraic typeclasses + +`Eq`, `Ord`, `Semigroup`, `Monoid`, multiple instances, immutable list concatenation and focused syntax helpers. + +## 0.3 — Typeclass laws — current -- reusable `EqLaws`, `OrdLaws`, `SemigroupLaws`, `MonoidLaws`; +- reusable `EqLaws`, `OrdLaws`, `SemigroupLaws`, `MonoidLaws` in test scope; - jqwik property-based testing; -- associativity, identity, reflexivity, symmetry, transitivity and ordering properties; -- lawful-instance test harness. +- properties for Integer, String, LocalDate, List and Person instances; +- explicit demonstration of an unlawful semigroup; +- normalized Person instance naming; +- law-focused documentation. ## 0.4 — Algebraic data types @@ -62,7 +50,7 @@ Planned generic folding and traversal abstractions. ## 0.8 — Instance resolution experiments -Possible explicit registry / `summon` experiments, kept outside the fundamental model. +Possible explicit registry / `summon` experiments kept outside the fundamental model. ## 0.9 — Automatic derivation diff --git a/docs/TYPECLASSES.md b/docs/TYPECLASSES.md index 686267c..f21728a 100644 --- a/docs/TYPECLASSES.md +++ b/docs/TYPECLASSES.md @@ -1,83 +1,54 @@ # Typeclasses in Java -## Concept +## External polymorphism -A typeclass describes behaviour for a type without requiring that type to inherit from or implement the behaviour interface. - -In this project a typeclass is represented by a small generic interface and an instance is an ordinary Java value implementing that interface. - -## Show +A typeclass describes behavior for a type without forcing the domain type to inherit from that behavior. ```java @FunctionalInterface -public interface Show { - String show(A value); +public interface Eq { + boolean eqv(A left, A right); } ``` -`Person` does not implement `Show`. Instead, separate values provide compact and verbose representations. +A `Person` record therefore remains a plain domain model while several `Eq` values may coexist. -## Eq +## Explicit dictionary passing -`Eq` represents selectable equality semantics: +Java does not provide Scala-style `given` / `using`. This project deliberately passes instances explicitly: ```java -Eq byName = - (left, right) -> left.name().equalsIgnoreCase(right.name()); +EqFunctions.equal(left, right, PersonInstances.EQ_NAME); ``` -This differs from `Object.equals`: multiple valid equality policies can coexist and callers choose one explicitly. - -## Ord - -`Ord` refines `Eq` with a comparison operation. Equality follows naturally when `compare(left, right) == 0`. - -A `Person` can therefore be ordered by age in one algorithm and by name in another without changing the record. - -## Semigroup +The approach is transparent, testable and requires no reflection or global registry. -A semigroup consists of a set of values and an associative `combine` operation. - -Examples include maximum over integers and string concatenation. - -Milestone 0.2 documents associativity but does not attempt to prove it with a few example-based unit tests. Systematic law verification belongs to 0.3. - -## Monoid +## Show -A monoid is a semigroup with an identity element. +`Show` represents presentation independently from the domain type. Multiple renderings can coexist. -Examples: +## Eq -| Type / operation | `combine` | identity | -| --- | --- | --- | -| Integer addition | `+` | `0` | -| Integer multiplication | `*` | `1` | -| String concatenation | concatenation | `""` | -| List concatenation | concatenation | `[]` | +`Eq` represents a chosen equivalence relation. A lawful equality is reflexive, symmetric and transitive. -## Explicit instance selection +## Ord -```java -var sum = MonoidFunctions.combineAll(values, IntegerInstances.ADDITION); -var product = MonoidFunctions.combineAll(values, IntegerInstances.MULTIPLICATION); -``` +`Ord` extends `Eq` and derives equality from zero comparison. Different orderings may intentionally induce different equivalence classes, such as ordering `Person` only by age. -The generic algorithm does not need to know anything about addition or multiplication. Behaviour is selected by supplying the appropriate dictionary value. +## Semigroup and Monoid -## Typeclasses versus inheritance +`Semigroup` describes associative combination. `Monoid` adds an identity element. -With inheritance, a domain type usually embeds one implementation of an interface. Typeclasses invert that relationship: external instances describe the type. +Milestone 0.3 no longer leaves these requirements as documentation only: property tests execute them through reusable law helpers. -This is especially useful when: +See [`LAWS.md`](LAWS.md). -- the domain type cannot be modified, such as `LocalDate`; -- several valid implementations exist for the same type; -- domain data should remain independent of presentation or algorithm policy. +## Multiple instances -## Typeclasses versus Strategy +The same Java type may have multiple useful instances. `Integer` has both addition and multiplication monoids. `Person` has several equality and ordering interpretations. -The Java representation resembles the Strategy pattern because behaviour is passed as an object. The typeclass perspective adds a stronger focus on generic capability interfaces, reusable instances, algebraic laws and composition across types. +This is a major difference from encoding behavior directly through inheritance on the data type. -## Deliberately missing in 0.2 +## Current limitation -There is no global registry, implicit search, `summon`, reflection, annotations, automatic derivation, higher-kinded type encoding, `Functor`, `Applicative`, or `Monad` yet. +Java has no native higher-kinded types. Milestone 0.3 intentionally does not attempt to solve that problem yet. HKT encoding is planned only after the first-order algebra and laws are stable. diff --git a/src/main/java/io/codeswarm/typeclasses/algebra/Semigroup.java b/src/main/java/io/codeswarm/typeclasses/algebra/Semigroup.java index 92d682d..5dd879c 100644 --- a/src/main/java/io/codeswarm/typeclasses/algebra/Semigroup.java +++ b/src/main/java/io/codeswarm/typeclasses/algebra/Semigroup.java @@ -8,9 +8,6 @@ * combine(combine(a, b), c) == combine(a, combine(b, c)) * } * - *

Milestone 0.2 documents this law but deliberately leaves systematic law - * verification to milestone 0.3.

- * * @param
the value type */ @FunctionalInterface diff --git a/src/main/java/io/codeswarm/typeclasses/examples/EqExample.java b/src/main/java/io/codeswarm/typeclasses/examples/EqExample.java index a09b1e7..a54445e 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/EqExample.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/EqExample.java @@ -16,7 +16,7 @@ private EqExample() { static void main() { final var alice30 = new Person("Alice", 30); final var alice40 = new Person("ALICE", 40); - System.out.println(EqFunctions.equal(alice30, alice40, PersonInstances.BY_ALL_FIELDS)); - System.out.println(EqFunctions.equal(alice30, alice40, PersonInstances.BY_NAME)); + System.out.println(EqFunctions.equal(alice30, alice40, PersonInstances.EQ_ALL_FIELDS)); + System.out.println(EqFunctions.equal(alice30, alice40, PersonInstances.EQ_NAME)); } } diff --git a/src/main/java/io/codeswarm/typeclasses/examples/OrdExample.java b/src/main/java/io/codeswarm/typeclasses/examples/OrdExample.java index cd5ab17..d4605e7 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/OrdExample.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/OrdExample.java @@ -16,7 +16,7 @@ private OrdExample() { static void main() { final var alice = new Person("Alice", 35); final var bob = new Person("Bob", 27); - System.out.println(OrdFunctions.min(alice, bob, PersonInstances.BY_AGE)); - System.out.println(OrdFunctions.min(alice, bob, PersonInstances.BY_NAME_ORDER)); + System.out.println(OrdFunctions.min(alice, bob, PersonInstances.ORD_AGE)); + System.out.println(OrdFunctions.min(alice, bob, PersonInstances.ORD_NAME)); } } diff --git a/src/main/java/io/codeswarm/typeclasses/examples/PersonInstances.java b/src/main/java/io/codeswarm/typeclasses/examples/PersonInstances.java index 4a91d90..6fe5937 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/PersonInstances.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/PersonInstances.java @@ -14,26 +14,26 @@ public final class PersonInstances { /** Compact representation containing only the person's name. */ - public static final Show COMPACT_SHOW = Person::name; + public static final Show SHOW_COMPACT = Person::name; /** Verbose representation containing both the person's name and age. */ - public static final Show VERBOSE_SHOW = + public static final Show SHOW_VERBOSE = person -> "%s (%d)".formatted(person.name(), person.age()); /** Equality based on both record components. */ - public static final Eq BY_ALL_FIELDS = + public static final Eq EQ_ALL_FIELDS = (left, right) -> left.name().equals(right.name()) && left.age() == right.age(); /** Case-insensitive equality based only on the person's name. */ - public static final Eq BY_NAME = + public static final Eq EQ_NAME = (left, right) -> left.name().equalsIgnoreCase(right.name()); - /** Orders people by age. */ - public static final Ord BY_AGE = + /** Orders people by age and therefore considers equal ages equivalent. */ + public static final Ord ORD_AGE = (left, right) -> Integer.compare(left.age(), right.age()); /** Orders people case-insensitively by name. */ - public static final Ord BY_NAME_ORDER = + public static final Ord ORD_NAME = (left, right) -> left.name().compareToIgnoreCase(right.name()); private PersonInstances() { diff --git a/src/main/java/io/codeswarm/typeclasses/examples/ShowExample.java b/src/main/java/io/codeswarm/typeclasses/examples/ShowExample.java index 9e449d5..1ae0e23 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/ShowExample.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/ShowExample.java @@ -19,8 +19,8 @@ private ShowExample() { */ static void main() { final var person = new Person("Alice", 30); - System.out.println(ShowFunctions.show(person, PersonInstances.COMPACT_SHOW)); - System.out.println(ShowFunctions.show(person, PersonInstances.VERBOSE_SHOW)); + System.out.println(ShowFunctions.show(person, PersonInstances.SHOW_COMPACT)); + System.out.println(ShowFunctions.show(person, PersonInstances.SHOW_VERBOSE)); System.out.println(ShowFunctions.show(42, IntegerInstances.SHOW)); System.out.println(ShowFunctions.show( LocalDate.of(2026, 9, 30), LocalDateInstances.ISO_SHOW)); diff --git a/src/test/java/io/codeswarm/typeclasses/examples/PersonInstancesTest.java b/src/test/java/io/codeswarm/typeclasses/examples/PersonInstancesTest.java index c91f05a..b41d7f2 100644 --- a/src/test/java/io/codeswarm/typeclasses/examples/PersonInstancesTest.java +++ b/src/test/java/io/codeswarm/typeclasses/examples/PersonInstancesTest.java @@ -13,8 +13,8 @@ class PersonInstancesTest { @Test void shouldSupportMultipleShowInstances() { final var person = new Person("Alice", 30); - assertEquals("Alice", PersonInstances.COMPACT_SHOW.show(person)); - assertEquals("Alice (30)", PersonInstances.VERBOSE_SHOW.show(person)); + assertEquals("Alice", PersonInstances.SHOW_COMPACT.show(person)); + assertEquals("Alice (30)", PersonInstances.SHOW_VERBOSE.show(person)); } /** Verifies that equality policy can be selected independently of Person. */ @@ -22,8 +22,8 @@ void shouldSupportMultipleShowInstances() { void shouldSupportMultipleEqualityDefinitions() { final var alice30 = new Person("Alice", 30); final var alice40 = new Person("ALICE", 40); - assertFalse(PersonInstances.BY_ALL_FIELDS.eqv(alice30, alice40)); - assertTrue(PersonInstances.BY_NAME.eqv(alice30, alice40)); + assertFalse(PersonInstances.EQ_ALL_FIELDS.eqv(alice30, alice40)); + assertTrue(PersonInstances.EQ_NAME.eqv(alice30, alice40)); } /** Verifies that ordering policy can be selected independently of Person. */ @@ -31,7 +31,7 @@ void shouldSupportMultipleEqualityDefinitions() { void shouldSupportMultipleOrderings() { final var alice = new Person("Alice", 35); final var bob = new Person("Bob", 27); - assertEquals(bob, PersonInstances.BY_AGE.min(alice, bob)); - assertEquals(alice, PersonInstances.BY_NAME_ORDER.min(alice, bob)); + assertEquals(bob, PersonInstances.ORD_AGE.min(alice, bob)); + assertEquals(alice, PersonInstances.ORD_NAME.min(alice, bob)); } } diff --git a/src/test/java/io/codeswarm/typeclasses/laws/EqLaws.java b/src/test/java/io/codeswarm/typeclasses/laws/EqLaws.java new file mode 100644 index 0000000..6ab60fe --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/laws/EqLaws.java @@ -0,0 +1,61 @@ +package io.codeswarm.typeclasses.laws; + +import io.codeswarm.typeclasses.core.Eq; + +/** + * Reusable executable laws for {@link Eq} instances. + * + *

The helpers return booleans so they can be used from both example-based + * JUnit tests and property-based jqwik tests without coupling the law model to + * a specific assertion library.

+ */ +public final class EqLaws { + + private EqLaws() { + throw new AssertionError("Law utility must not be instantiated"); + } + + /** + * Checks reflexivity: {@code a ~ a}. + * + * @param value tested value + * @param eq equality instance + * @param
value type + * @return {@code true} when reflexivity holds for the supplied value + */ + public static boolean reflexive(final A value, final Eq eq) { + return eq.eqv(value, value); + } + + /** + * Checks symmetry: {@code a ~ b} iff {@code b ~ a}. + * + * @param left left value + * @param right right value + * @param eq equality instance + * @param value type + * @return {@code true} when symmetry holds for the supplied pair + */ + public static boolean symmetric(final A left, final A right, final Eq eq) { + return eq.eqv(left, right) == eq.eqv(right, left); + } + + /** + * Checks transitivity: when {@code a ~ b} and {@code b ~ c}, then + * {@code a ~ c} must also hold. + * + * @param first first value + * @param second second value + * @param third third value + * @param eq equality instance + * @param value type + * @return {@code true} when the transitivity implication holds + */ + public static boolean transitive( + final A first, + final A second, + final A third, + final Eq eq) { + return !(eq.eqv(first, second) && eq.eqv(second, third)) || eq.eqv(first, third); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/laws/MonoidLaws.java b/src/test/java/io/codeswarm/typeclasses/laws/MonoidLaws.java new file mode 100644 index 0000000..e4489e3 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/laws/MonoidLaws.java @@ -0,0 +1,66 @@ +package io.codeswarm.typeclasses.laws; + +import io.codeswarm.typeclasses.algebra.Monoid; +import io.codeswarm.typeclasses.core.Eq; + +/** + * Reusable executable laws for {@link Monoid} instances. + */ +public final class MonoidLaws { + + private MonoidLaws() { + throw new AssertionError("Law utility must not be instantiated"); + } + + /** + * Checks the inherited semigroup associativity law. + * + * @param first first value + * @param second second value + * @param third third value + * @param monoid monoid instance + * @param eq equality used to compare results + * @param value type + * @return {@code true} when associativity holds + */ + public static boolean associative( + final A first, + final A second, + final A third, + final Monoid monoid, + final Eq eq) { + return SemigroupLaws.associative(first, second, third, monoid, eq); + } + + /** + * Checks the left identity law: {@code empty <> a == a}. + * + * @param value tested value + * @param monoid monoid instance + * @param eq equality used to compare results + * @param value type + * @return {@code true} when left identity holds + */ + public static boolean leftIdentity( + final A value, + final Monoid monoid, + final Eq eq) { + return eq.eqv(monoid.combine(monoid.empty(), value), value); + } + + /** + * Checks the right identity law: {@code a <> empty == a}. + * + * @param value tested value + * @param monoid monoid instance + * @param eq equality used to compare results + * @param value type + * @return {@code true} when right identity holds + */ + public static boolean rightIdentity( + final A value, + final Monoid monoid, + final Eq eq) { + return eq.eqv(monoid.combine(value, monoid.empty()), value); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/laws/OrdLaws.java b/src/test/java/io/codeswarm/typeclasses/laws/OrdLaws.java new file mode 100644 index 0000000..889f758 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/laws/OrdLaws.java @@ -0,0 +1,77 @@ +package io.codeswarm.typeclasses.laws; + +import io.codeswarm.typeclasses.core.Ord; + +/** + * Reusable executable laws for {@link Ord} instances. + */ +public final class OrdLaws { + + private OrdLaws() { + throw new AssertionError("Law utility must not be instantiated"); + } + + /** + * Checks reflexivity of ordering: comparing a value with itself yields zero. + * + * @param value tested value + * @param ord ordering instance + * @param value type + * @return {@code true} when reflexivity holds + */ + public static boolean reflexive(final A value, final Ord ord) { + return ord.compare(value, value) == 0; + } + + /** + * Checks sign antisymmetry of comparison results. + * + * @param left left value + * @param right right value + * @param ord ordering instance + * @param value type + * @return {@code true} when reversing arguments reverses the comparison sign + */ + public static boolean signAntisymmetric( + final A left, + final A right, + final Ord ord) { + return Integer.signum(ord.compare(left, right)) == -Integer.signum(ord.compare(right, left)); + } + + /** + * Checks transitivity of the less-than-or-equal relation. + * + * @param first first value + * @param second second value + * @param third third value + * @param ord ordering instance + * @param value type + * @return {@code true} when the transitivity implication holds + */ + public static boolean transitive( + final A first, + final A second, + final A third, + final Ord ord) { + final boolean firstBeforeSecond = ord.compare(first, second) <= 0; + final boolean secondBeforeThird = ord.compare(second, third) <= 0; + return !(firstBeforeSecond && secondBeforeThird) || ord.compare(first, third) <= 0; + } + + /** + * Checks that {@link Ord#eqv(Object, Object)} agrees with zero comparison. + * + * @param left left value + * @param right right value + * @param ord ordering instance + * @param value type + * @return {@code true} when equality and comparison agree + */ + public static boolean consistentWithEquality( + final A left, + final A right, + final Ord ord) { + return ord.eqv(left, right) == (ord.compare(left, right) == 0); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/laws/SemigroupLaws.java b/src/test/java/io/codeswarm/typeclasses/laws/SemigroupLaws.java new file mode 100644 index 0000000..34fb691 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/laws/SemigroupLaws.java @@ -0,0 +1,36 @@ +package io.codeswarm.typeclasses.laws; + +import io.codeswarm.typeclasses.algebra.Semigroup; +import io.codeswarm.typeclasses.core.Eq; + +/** + * Reusable executable laws for {@link Semigroup} instances. + */ +public final class SemigroupLaws { + + private SemigroupLaws() { + throw new AssertionError("Law utility must not be instantiated"); + } + + /** + * Checks associativity of a semigroup operation. + * + * @param first first value + * @param second second value + * @param third third value + * @param semigroup semigroup instance + * @param eq equality used to compare both association results + * @param value type + * @return {@code true} when associativity holds for the supplied values + */ + public static boolean associative( + final A first, + final A second, + final A third, + final Semigroup semigroup, + final Eq eq) { + final A leftAssociated = semigroup.combine(semigroup.combine(first, second), third); + final A rightAssociated = semigroup.combine(first, semigroup.combine(second, third)); + return eq.eqv(leftAssociated, rightAssociated); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/laws/UnlawfulInstancesTest.java b/src/test/java/io/codeswarm/typeclasses/laws/UnlawfulInstancesTest.java new file mode 100644 index 0000000..23e0a01 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/laws/UnlawfulInstancesTest.java @@ -0,0 +1,30 @@ +package io.codeswarm.typeclasses.laws; + +import io.codeswarm.typeclasses.algebra.Semigroup; +import io.codeswarm.typeclasses.instances.IntegerInstances; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * Demonstrates that implementing a typeclass interface does not automatically + * make an instance lawful. + */ +class UnlawfulInstancesTest { + + /** + * Shows that integer subtraction is not associative and therefore cannot be + * a lawful {@link Semigroup} operation. + */ + @Test + void subtractionShouldFailAssociativity() { + final Semigroup subtraction = (left, right) -> left - right; + + assertFalse(SemigroupLaws.associative( + 10, + 3, + 2, + subtraction, + IntegerInstances.EQ)); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/properties/IntegerInstancesProperties.java b/src/test/java/io/codeswarm/typeclasses/properties/IntegerInstancesProperties.java new file mode 100644 index 0000000..8fcfe76 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/properties/IntegerInstancesProperties.java @@ -0,0 +1,73 @@ +package io.codeswarm.typeclasses.properties; + +import io.codeswarm.typeclasses.instances.IntegerInstances; +import io.codeswarm.typeclasses.laws.EqLaws; +import io.codeswarm.typeclasses.laws.MonoidLaws; +import io.codeswarm.typeclasses.laws.OrdLaws; +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Property-based law verification for {@link IntegerInstances}. + */ +class IntegerInstancesProperties { + + /** Verifies the reflexivity law of integer equality. */ + @Property + void equalityIsReflexive(@ForAll final int value) { + assertTrue(EqLaws.reflexive(value, IntegerInstances.EQ)); + } + + /** Verifies the symmetry law of integer equality. */ + @Property + void equalityIsSymmetric(@ForAll final int left, @ForAll final int right) { + assertTrue(EqLaws.symmetric(left, right, IntegerInstances.EQ)); + } + + /** Verifies the transitivity law of integer equality. */ + @Property + void equalityIsTransitive( + @ForAll final int first, + @ForAll final int second, + @ForAll final int third) { + assertTrue(EqLaws.transitive(first, second, third, IntegerInstances.EQ)); + } + + /** Verifies the integer ordering laws. */ + @Property + void orderingIsLawful( + @ForAll final int first, + @ForAll final int second, + @ForAll final int third) { + assertTrue(OrdLaws.reflexive(first, IntegerInstances.ORD)); + assertTrue(OrdLaws.signAntisymmetric(first, second, IntegerInstances.ORD)); + assertTrue(OrdLaws.transitive(first, second, third, IntegerInstances.ORD)); + assertTrue(OrdLaws.consistentWithEquality(first, second, IntegerInstances.ORD)); + } + + /** Verifies associativity and identity for integer addition. */ + @Property + void additionIsALawfulMonoid( + @ForAll final int first, + @ForAll final int second, + @ForAll final int third) { + assertTrue(MonoidLaws.associative( + first, second, third, IntegerInstances.ADDITION, IntegerInstances.EQ)); + assertTrue(MonoidLaws.leftIdentity(first, IntegerInstances.ADDITION, IntegerInstances.EQ)); + assertTrue(MonoidLaws.rightIdentity(first, IntegerInstances.ADDITION, IntegerInstances.EQ)); + } + + /** Verifies associativity and identity for integer multiplication. */ + @Property + void multiplicationIsALawfulMonoid( + @ForAll final int first, + @ForAll final int second, + @ForAll final int third) { + assertTrue(MonoidLaws.associative( + first, second, third, IntegerInstances.MULTIPLICATION, IntegerInstances.EQ)); + assertTrue(MonoidLaws.leftIdentity(first, IntegerInstances.MULTIPLICATION, IntegerInstances.EQ)); + assertTrue(MonoidLaws.rightIdentity(first, IntegerInstances.MULTIPLICATION, IntegerInstances.EQ)); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/properties/ListInstancesProperties.java b/src/test/java/io/codeswarm/typeclasses/properties/ListInstancesProperties.java new file mode 100644 index 0000000..95ee89c --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/properties/ListInstancesProperties.java @@ -0,0 +1,49 @@ +package io.codeswarm.typeclasses.properties; + +import io.codeswarm.typeclasses.algebra.Monoid; +import io.codeswarm.typeclasses.core.Eq; +import io.codeswarm.typeclasses.instances.ListInstances; +import io.codeswarm.typeclasses.laws.MonoidLaws; +import net.jqwik.api.Arbitraries; +import net.jqwik.api.Arbitrary; +import net.jqwik.api.ForAll; +import net.jqwik.api.Provide; +import net.jqwik.api.Property; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Property-based law verification for list concatenation. + */ +class ListInstancesProperties { + + private static final Monoid> CONCATENATION = ListInstances.concatenation(); + private static final Eq> LIST_EQ = List::equals; + + /** + * Supplies small immutable integer lists to keep shrinking and diagnostics concise. + * + * @return arbitrary immutable lists + */ + @Provide + Arbitrary> integerLists() { + return Arbitraries.integers() + .between(-1_000, 1_000) + .list() + .ofMaxSize(20) + .map(List::copyOf); + } + + /** Verifies list concatenation as a lawful monoid. */ + @Property + void concatenationIsALawfulMonoid( + @ForAll("integerLists") final List first, + @ForAll("integerLists") final List second, + @ForAll("integerLists") final List third) { + assertTrue(MonoidLaws.associative(first, second, third, CONCATENATION, LIST_EQ)); + assertTrue(MonoidLaws.leftIdentity(first, CONCATENATION, LIST_EQ)); + assertTrue(MonoidLaws.rightIdentity(first, CONCATENATION, LIST_EQ)); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/properties/LocalDateInstancesProperties.java b/src/test/java/io/codeswarm/typeclasses/properties/LocalDateInstancesProperties.java new file mode 100644 index 0000000..5bfc4fc --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/properties/LocalDateInstancesProperties.java @@ -0,0 +1,55 @@ +package io.codeswarm.typeclasses.properties; + +import io.codeswarm.typeclasses.instances.LocalDateInstances; +import io.codeswarm.typeclasses.laws.EqLaws; +import io.codeswarm.typeclasses.laws.OrdLaws; +import net.jqwik.api.Arbitraries; +import net.jqwik.api.Arbitrary; +import net.jqwik.api.ForAll; +import net.jqwik.api.Provide; +import net.jqwik.api.Property; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Property-based law verification for {@link LocalDateInstances}. + */ +class LocalDateInstancesProperties { + + /** + * Supplies dates from a deliberately broad but safe epoch-day range. + * + * @return arbitrary local dates + */ + @Provide + Arbitrary dates() { + return Arbitraries.longs() + .between(-365_000L, 365_000L) + .map(LocalDate::ofEpochDay); + } + + /** Verifies equality laws for dates. */ + @Property + void equalityIsLawful( + @ForAll("dates") final LocalDate first, + @ForAll("dates") final LocalDate second, + @ForAll("dates") final LocalDate third) { + assertTrue(EqLaws.reflexive(first, LocalDateInstances.EQ)); + assertTrue(EqLaws.symmetric(first, second, LocalDateInstances.EQ)); + assertTrue(EqLaws.transitive(first, second, third, LocalDateInstances.EQ)); + } + + /** Verifies chronological ordering laws for dates. */ + @Property + void orderingIsLawful( + @ForAll("dates") final LocalDate first, + @ForAll("dates") final LocalDate second, + @ForAll("dates") final LocalDate third) { + assertTrue(OrdLaws.reflexive(first, LocalDateInstances.ORD)); + assertTrue(OrdLaws.signAntisymmetric(first, second, LocalDateInstances.ORD)); + assertTrue(OrdLaws.transitive(first, second, third, LocalDateInstances.ORD)); + assertTrue(OrdLaws.consistentWithEquality(first, second, LocalDateInstances.ORD)); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/properties/PersonInstancesProperties.java b/src/test/java/io/codeswarm/typeclasses/properties/PersonInstancesProperties.java new file mode 100644 index 0000000..d03f52d --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/properties/PersonInstancesProperties.java @@ -0,0 +1,81 @@ +package io.codeswarm.typeclasses.properties; + +import io.codeswarm.typeclasses.examples.Person; +import io.codeswarm.typeclasses.examples.PersonInstances; +import io.codeswarm.typeclasses.laws.EqLaws; +import io.codeswarm.typeclasses.laws.OrdLaws; +import net.jqwik.api.Arbitraries; +import net.jqwik.api.Arbitrary; +import net.jqwik.api.Combinators; +import net.jqwik.api.ForAll; +import net.jqwik.api.Provide; +import net.jqwik.api.Property; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Property-based law verification for the example {@link Person} instances. + */ +class PersonInstancesProperties { + + /** + * Supplies valid example people with compact names and realistic ages. + * + * @return arbitrary people + */ + @Provide + Arbitrary people() { + final Arbitrary names = Arbitraries.strings() + .alpha() + .ofMinLength(1) + .ofMaxLength(20); + final Arbitrary ages = Arbitraries.integers().between(0, 130); + return Combinators.combine(names, ages).as(Person::new); + } + + /** Verifies equality based on all fields. */ + @Property + void allFieldsEqualityIsLawful( + @ForAll("people") final Person first, + @ForAll("people") final Person second, + @ForAll("people") final Person third) { + assertTrue(EqLaws.reflexive(first, PersonInstances.EQ_ALL_FIELDS)); + assertTrue(EqLaws.symmetric(first, second, PersonInstances.EQ_ALL_FIELDS)); + assertTrue(EqLaws.transitive(first, second, third, PersonInstances.EQ_ALL_FIELDS)); + } + + /** Verifies case-insensitive equality based on name. */ + @Property + void nameEqualityIsLawful( + @ForAll("people") final Person first, + @ForAll("people") final Person second, + @ForAll("people") final Person third) { + assertTrue(EqLaws.reflexive(first, PersonInstances.EQ_NAME)); + assertTrue(EqLaws.symmetric(first, second, PersonInstances.EQ_NAME)); + assertTrue(EqLaws.transitive(first, second, third, PersonInstances.EQ_NAME)); + } + + /** Verifies ordering by age. */ + @Property + void ageOrderingIsLawful( + @ForAll("people") final Person first, + @ForAll("people") final Person second, + @ForAll("people") final Person third) { + assertTrue(OrdLaws.reflexive(first, PersonInstances.ORD_AGE)); + assertTrue(OrdLaws.signAntisymmetric(first, second, PersonInstances.ORD_AGE)); + assertTrue(OrdLaws.transitive(first, second, third, PersonInstances.ORD_AGE)); + assertTrue(OrdLaws.consistentWithEquality(first, second, PersonInstances.ORD_AGE)); + } + + /** Verifies case-insensitive ordering by name. */ + @Property + void nameOrderingIsLawful( + @ForAll("people") final Person first, + @ForAll("people") final Person second, + @ForAll("people") final Person third) { + assertTrue(OrdLaws.reflexive(first, PersonInstances.ORD_NAME)); + assertTrue(OrdLaws.signAntisymmetric(first, second, PersonInstances.ORD_NAME)); + assertTrue(OrdLaws.transitive(first, second, third, PersonInstances.ORD_NAME)); + assertTrue(OrdLaws.consistentWithEquality(first, second, PersonInstances.ORD_NAME)); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/properties/StringInstancesProperties.java b/src/test/java/io/codeswarm/typeclasses/properties/StringInstancesProperties.java new file mode 100644 index 0000000..3d96a03 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/properties/StringInstancesProperties.java @@ -0,0 +1,63 @@ +package io.codeswarm.typeclasses.properties; + +import io.codeswarm.typeclasses.instances.StringInstances; +import io.codeswarm.typeclasses.laws.EqLaws; +import io.codeswarm.typeclasses.laws.MonoidLaws; +import io.codeswarm.typeclasses.laws.OrdLaws; +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Property-based law verification for {@link StringInstances}. + */ +class StringInstancesProperties { + + /** Verifies the standard string equality laws. */ + @Property + void equalityIsLawful( + @ForAll final String first, + @ForAll final String second, + @ForAll final String third) { + assertTrue(EqLaws.reflexive(first, StringInstances.EQ)); + assertTrue(EqLaws.symmetric(first, second, StringInstances.EQ)); + assertTrue(EqLaws.transitive(first, second, third, StringInstances.EQ)); + } + + /** Verifies the case-sensitive lexicographic ordering laws. */ + @Property + void lexicographicOrderingIsLawful( + @ForAll final String first, + @ForAll final String second, + @ForAll final String third) { + assertTrue(OrdLaws.reflexive(first, StringInstances.LEXICOGRAPHIC_ORD)); + assertTrue(OrdLaws.signAntisymmetric(first, second, StringInstances.LEXICOGRAPHIC_ORD)); + assertTrue(OrdLaws.transitive(first, second, third, StringInstances.LEXICOGRAPHIC_ORD)); + assertTrue(OrdLaws.consistentWithEquality(first, second, StringInstances.LEXICOGRAPHIC_ORD)); + } + + /** Verifies the case-insensitive lexicographic ordering laws. */ + @Property + void caseInsensitiveOrderingIsLawful( + @ForAll final String first, + @ForAll final String second, + @ForAll final String third) { + assertTrue(OrdLaws.reflexive(first, StringInstances.CASE_INSENSITIVE_ORD)); + assertTrue(OrdLaws.signAntisymmetric(first, second, StringInstances.CASE_INSENSITIVE_ORD)); + assertTrue(OrdLaws.transitive(first, second, third, StringInstances.CASE_INSENSITIVE_ORD)); + assertTrue(OrdLaws.consistentWithEquality(first, second, StringInstances.CASE_INSENSITIVE_ORD)); + } + + /** Verifies string concatenation as a lawful monoid. */ + @Property + void concatenationIsALawfulMonoid( + @ForAll final String first, + @ForAll final String second, + @ForAll final String third) { + assertTrue(MonoidLaws.associative( + first, second, third, StringInstances.CONCATENATION, StringInstances.EQ)); + assertTrue(MonoidLaws.leftIdentity(first, StringInstances.CONCATENATION, StringInstances.EQ)); + assertTrue(MonoidLaws.rightIdentity(first, StringInstances.CONCATENATION, StringInstances.EQ)); + } +}