diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53ae31e..9d089cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,12 +6,8 @@ on: pull_request: branches: [ main ] -permissions: - contents: read - jobs: build: - name: Java ${{ matrix.java }} runs-on: ubuntu-latest strategy: fail-fast: false @@ -20,19 +16,16 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v4 - name: Set up Java uses: actions/setup-java@v5 with: distribution: temurin java-version: ${{ matrix.java }} - cache: gradle - name: Set up Gradle - uses: gradle/actions/setup-gradle@v5 - with: - gradle-version: '9.8.0' + uses: gradle/actions/setup-gradle@v4 - name: Verify - run: gradle clean check javadoc + run: ./gradlew clean check javadoc diff --git a/.gitignore b/.gitignore index 944480e..bdf6850 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,11 @@ .gradle/ build/ out/ +*.class -# IntelliJ IDEA .idea/ -*.iws *.iml -*.ipr - -# Eclipse -.classpath -.project -.settings/ -bin/ - -# NetBeans -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -# VS Code .vscode/ -# OS files .DS_Store Thumbs.db diff --git a/CHANGELOG.md b/CHANGELOG.md index a66b004..321dfdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,49 +1,67 @@ # Changelog -All notable changes to this project are documented in this file. +All notable changes to the project are documented in this file. -The project follows an incremental milestone model. Version 0.1 is treated as the original proof of concept; 0.1.1 is its first hardened release. - -## [0.1.1] - 2026-09-30 +## 0.2.0 — Basic Algebraic Typeclasses ### Added -- Java 21 toolchain baseline. -- `Show` as the minimal first-order typeclass contract. -- `TypeClassFunctions.show(...)` demonstrating explicit dictionary passing. -- `StringInstances`, `IntegerInstances`, and `LocalDateInstances`. -- `Person` example record with domain validation. -- compact and verbose `Show` instances. -- executable `ShowExample`. -- JUnit tests for the core API, standard instances, domain validation, and multiple-instance behaviour. -- JaCoCo report generation. -- sources JAR and Javadoc JAR generation. -- GitHub Actions build matrix for Java 21, 25, and 27. -- `docs/ARCHITECTURE.md`. -- `docs/TYPECLASSES.md`. -- `docs/ROADMAP.md`. -- Apache License 2.0. +- `Eq` with derived inequality support. +- `Ord` extending `Eq` with comparison, ordering predicates, minimum and maximum. +- `Semigroup` representing associative combination. +- `Monoid` extending `Semigroup` with an identity element. +- Dedicated `ShowFunctions`, `EqFunctions`, `OrdFunctions`, `SemigroupFunctions`, and `MonoidFunctions` syntax helpers. +- Integer equality, ordering, addition monoid and multiplication monoid. +- String equality, case-sensitive and case-insensitive ordering, and concatenation monoid. +- `LocalDate` equality and chronological ordering. +- Immutable-style generic list concatenation monoid. +- Multiple `Eq` and `Ord` instances. +- Executable examples for equality, ordering, semigroup and monoid usage. +- Focused tests for all new typeclasses, syntax helpers and instance families. +- `docs/ALGEBRA.md`. +- Detailed milestone 0.2 implementation task list. ### Changed -- Replaced the original GitLab template README with project-specific documentation. -- Renamed the Gradle root project to `java-typeclasses`. -- Changed the Gradle plugin from `java` to `java-library`. -- Set project version to `0.1.1`. -- Updated the Gradle wrapper configuration target from Gradle 8.2 to 9.8.0. -- Standardised source packages under `io.codeswarm.typeclasses`. -- Separated reusable core abstractions, platform instances, and educational examples. -- Enabled compiler linting and UTF-8 consistently. +- Project version advanced from `0.1.1` to `0.2.0`. +- Generic helper architecture changed from one growing `TypeClassFunctions` class to one small syntax class per typeclass family. +- `ShowExample` now uses `ShowFunctions`. +- README now documents the complete basic algebra model and multiple-instance examples. +- Architecture, typeclass documentation and roadmap were updated for milestone 0.2. +- CI verification now runs `clean check javadoc` for Java 21, 25 and 27. ### Removed -- Placeholder GitLab onboarding documentation. -- Legacy/unused source structure from the proof-of-concept milestone. -- Unused IDE/build artefacts and obsolete generated files from the milestone source tree. -- Premature abstractions not required to explain milestone 0.1.1. +- `TypeClassFunctions`, superseded by focused syntax helpers. +- The old aggregated tests from 0.1.1 that were replaced by smaller type-specific test classes. +- No obsolete pre-hardening scripts, generated build output or IDE files are included in the source distribution. ### Design notes -- Typeclass instances remain independent from domain classes. -- Instance dependencies are passed explicitly. -- No reflection, service locator, registry, dependency-injection framework, HKT encoding, or automatic derivation is introduced in this release. +- 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. +- HKT, ADTs, `Functor`, `Applicative` and `Monad` remain out of scope for YAGNI reasons. + +## 0.1.1 — Foundation Hardening + +### Added + +- Java 21 toolchain baseline. +- `Show` as the minimal typeclass example. +- Explicit dictionary-passing helper. +- Standard Show instances for Integer, String and LocalDate. +- Person example with compact and verbose rendering instances. +- Unit tests, JaCoCo reporting, Javadoc generation and GitHub Actions CI. +- Architecture, typeclass, roadmap and implementation documentation. +- Apache License 2.0. + +### Changed + +- Reorganized the original prototype into coherent `core`, `instances` and `examples` packages. +- Replaced the placeholder GitLab README with project-specific documentation. + +## 0.1 — Original Prototype + +Initial repository state, subsequently treated as milestone 0.1. diff --git a/README.md b/README.md index 8e46ebb..7959502 100644 --- a/README.md +++ b/README.md @@ -1,174 +1,201 @@ # Java Typeclasses -An educational implementation of the **typeclass pattern in modern Java**. +An educational implementation of **typeclass-oriented programming in modern Java**. -Milestone **0.1.1** is a foundation-hardening release. It keeps the code deliberately small and explicit while establishing the architecture, documentation, tests, and build conventions used by later milestones. +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. -## Goals - -The project demonstrates how behaviour can be defined independently from domain types and passed explicitly to generic algorithms. +## Requirements -Milestone 0.1.1 focuses on: +- Java 21+ +- Gradle 9.8.0 (wrapper configuration is provided) -- explicit dictionary passing; -- separation of domain data from typeclass instances; -- multiple instances for the same domain type; -- instances for types that cannot be modified, such as JDK classes; -- a clean Java 21 baseline; -- tests, Javadoc, JaCoCo, and CI; -- a stable foundation for future algebraic typeclasses. +The CI matrix verifies Java 21, 25 and 27. -It intentionally does **not** yet implement `Eq`, `Ord`, `Semigroup`, `Monoid`, higher-kinded type encodings, `Functor`, `Applicative`, `Monad`, instance registries, reflection-based resolution, or automatic derivation. Those belong to later milestones. +## What milestone 0.2 contains -## What is a typeclass here? +```text +Show -Java does not provide native typeclasses. This project models the pattern using ordinary generic interfaces whose implementations are values passed to generic algorithms. +Eq + ▲ + │ +Ord -```java -@FunctionalInterface -public interface Show { - String show(A value); -} +Semigroup + ▲ + │ + Monoid ``` -A domain model does not implement `Show`: +- `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. + +This milestone intentionally does **not** add HKT encoding, `Functor`, `Applicative`, `Monad`, reflection-based lookup, a global registry, annotations, or automatic derivation. + +## Core idea + +A domain type does not implement its typeclass: ```java public record Person(String name, int age) { } ``` -Instead, behaviour is defined separately: +Instead, behaviour exists independently: ```java -public static final Show COMPACT_SHOW = Person::name; +public static final Eq BY_NAME = + (left, right) -> left.name().equalsIgnoreCase(right.name()); -public static final Show VERBOSE_SHOW = - person -> "%s (%d)".formatted(person.name(), person.age()); +public static final Ord BY_AGE = + (left, right) -> Integer.compare(left.age(), right.age()); ``` -Both instances can coexist: +The same domain value can therefore have several meaningful behaviors without modifying `Person`. -```java -var person = new Person("Alice", 30); +## Multiple instances for one type + +`Integer` demonstrates why typeclass instances are values rather than behavior embedded in the data type: -COMPACT_SHOW.show(person); // Alice -VERBOSE_SHOW.show(person); // Alice (30) +```java +IntegerInstances.ADDITION +IntegerInstances.MULTIPLICATION ``` -This separation is the key design decision of the project. +Both are lawful `Monoid` instances but have different operations and identities: -## Explicit dictionary passing +```text +addition combine = + empty = 0 +multiplication combine = * empty = 1 +``` -Generic code receives the behaviour it needs explicitly: +A generic algorithm can select behavior explicitly: ```java -var result = TypeClassFunctions.show(person, PersonInstances.VERBOSE_SHOW); +var numbers = List.of(1, 2, 3, 4); + +var sum = MonoidFunctions.combineAll( + numbers, + IntegerInstances.ADDITION); + +var product = MonoidFunctions.combineAll( + numbers, + IntegerInstances.MULTIPLICATION); ``` -No reflection, global registry, dependency-injection framework, or hidden instance lookup is required. +Results: -## Why not inheritance? +```text +10 +24 +``` -With inheritance, behaviour is usually attached to the model itself. That makes it difficult or impossible to: +## Semigroup versus Monoid -- define a typeclass for a class you cannot modify; -- define several equally valid instances for one type; -- keep domain data independent from unrelated behaviour. +A `Semigroup` only knows how to combine two values. Consequently `SemigroupFunctions.combineAll` requires a non-empty iterable. -For example, this project defines `Show` even though `LocalDate` is a JDK class. +A `Monoid` additionally defines `empty()`, so `MonoidFunctions.combineAll` also works for an empty iterable and returns the identity value. -## Project structure +## Package structure ```text -src/main/java/io/codeswarm/typeclasses/ -├── core/ -│ ├── Show.java -│ └── TypeClassFunctions.java -├── instances/ -│ ├── IntegerInstances.java -│ ├── LocalDateInstances.java -│ └── StringInstances.java -└── examples/ - ├── Person.java - ├── PersonInstances.java - └── ShowExample.java +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 ``` -Supporting documentation is stored in `docs/`. +## Design principles -## Requirements +### SOLID -- Java 21 or newer -- Gradle 9.8.0 +- **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 project uses Java 21 as its source/toolchain baseline. CI verifies the project on Java 21, 25, and 27. +### KISS / DRY / YAGNI -## Build +- 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. -```bash -gradle clean build -``` - -When this milestone is applied to the original repository, regenerate its existing Gradle Wrapper for 9.8.0 with `gradle wrapper --gradle-version 9.8.0`. The source archive includes the target wrapper properties, but not a regenerated binary wrapper JAR. +## Build -## Tests and quality checks +With a generated Gradle wrapper: ```bash -gradle clean check javadoc -``` - -JaCoCo reports are generated under: - -```text -build/reports/jacoco/test/html/ +./gradlew clean check javadoc ``` -Javadoc is generated under: +If the binary wrapper JAR is not present after extracting the source archive, regenerate it once with a local Gradle installation: -```text -build/docs/javadoc/ +```bash +gradle wrapper --gradle-version 9.8.0 ``` -## Running the example +## Examples -Compile the project and run: +After compiling, run the example classes: ```text -io.codeswarm.typeclasses.examples.ShowExample +ShowExample +EqExample +OrdExample +SemigroupExample +MonoidExample ``` -Expected output: - -```text -Alice -Alice (30) -42 -2026-09-30 -``` - -## Design principles - -The milestone follows: - -- **SOLID** — especially SRP and dependency inversion through explicit behavioural interfaces; -- **KISS** — no registry, reflection, DI framework, or HKT encoding yet; -- **DRY** — shared generic behaviour lives in small reusable abstractions; -- **YAGNI** — only abstractions required to explain the current milestone are implemented; -- **composition over inheritance** — behaviour is supplied as values rather than embedded into domain hierarchies. - -See [Architecture](docs/ARCHITECTURE.md) for details. - -## Roadmap +## Documentation -The planned evolution is documented in [ROADMAP.md](docs/ROADMAP.md). +- [`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. -The next milestone, **0.2**, is expected to introduce the first algebraic typeclasses such as `Eq`, `Ord`, `Semigroup`, and `Monoid` while keeping the explicit instance model established here. +## Next milestone -## Changelog +Milestone **0.3** will focus on typeclass laws and property-based testing: -See [CHANGELOG.md](CHANGELOG.md). +- `Eq` laws; +- `Ord` laws; +- `Semigroup` associativity; +- `Monoid` associativity and identity; +- reusable law-test harness; +- jqwik property tests. ## License -Apache License 2.0. See [LICENSE](LICENSE). +Apache License 2.0. See [`LICENSE`](LICENSE). diff --git a/build.gradle b/build.gradle index ff7b2d7..b9bf813 100644 --- a/build.gradle +++ b/build.gradle @@ -4,9 +4,9 @@ plugins { } group = 'io.codeswarm' -version = '0.1.1' +version = '0.2.0' -description = 'Educational implementation of the typeclass pattern in modern Java.' +description = 'Educational implementation of typeclass-oriented programming and basic algebraic typeclasses in modern Java.' java { toolchain { diff --git a/docs/ALGEBRA.md b/docs/ALGEBRA.md new file mode 100644 index 0000000..5124532 --- /dev/null +++ b/docs/ALGEBRA.md @@ -0,0 +1,69 @@ +# Basic Algebra — Milestone 0.2 + +Milestone 0.2 introduces two algebraic structures: `Semigroup` and `Monoid`. + +## Semigroup + +A semigroup is a type together with an associative binary operation: + +```text +combine(combine(a, b), c) +== +combine(a, combine(b, c)) +``` + +The interface is intentionally minimal: + +```java +@FunctionalInterface +public interface Semigroup { + A combine(A left, A right); +} +``` + +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. + +## Monoid + +A monoid extends a semigroup with an identity element `empty()`. + +In addition to associativity, it must satisfy: + +```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 + +Strings form a monoid under concatenation with `""` as the identity. + +## List concatenation + +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 testing + +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. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5f0ec79..76c23d8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,116 +1,103 @@ -# Architecture — milestone 0.1.1 +# Architecture — Milestone 0.2 ## Purpose -Milestone 0.1.1 establishes the smallest architecture that can explain and safely evolve the typeclass pattern in Java. +Milestone 0.2 evolves the 0.1.1 foundation into a small algebraic typeclass library while deliberately retaining explicit, understandable mechanics. -## Dependency direction +## Layers ```text -Domain data Typeclass contract - Person Show - │ │ - │ │ - └──────────┐ ┌────────┘ - ▼ ▼ - Typeclass instance - Show - │ - ▼ - Generic algorithm - TypeClassFunctions.show +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) ``` -The domain model does not depend on the typeclass contract or on a concrete instance. +## Package responsibilities -## Packages +### `core` -### `io.codeswarm.typeclasses.core` +Contains small capability interfaces that are not specifically combination algebras: `Show`, `Eq`, and `Ord`. -Contains the minimal reusable API of the milestone: +### `algebra` -- `Show` — the typeclass contract; -- `TypeClassFunctions` — generic algorithms consuming typeclass instances. +Contains algebraic combination structures: `Semigroup` and `Monoid`. -### `io.codeswarm.typeclasses.instances` +### `syntax` -Contains reusable instances for JDK/platform types. Instances are grouped by target type to avoid a global god-class. +Contains reusable algorithms that consume typeclass instances explicitly. A separate class exists for each family to avoid a growing generic utility class. -### `io.codeswarm.typeclasses.examples` +### `instances` -Contains educational domain examples. Example-only code does not leak into the core package. +Contains reusable instances for JDK types and generic instance factories such as list concatenation. -## Architectural rules +### `examples` -1. **Domain types do not implement typeclasses.** - A typeclass instance is separate from the represented model. +Contains the `Person` domain type, its domain-specific instances, and executable examples. -2. **Dependencies are explicit.** - Generic algorithms receive instances as method arguments. +## Type relationships -3. **No hidden global resolution in 0.1.1.** - There is no service locator, registry, reflection, annotation processing, or dependency-injection container. +`Ord` extends `Eq` because a total ordering determines equality through `compare(a, b) == 0`. -4. **Instances are immutable values.** - Stateless instances are exposed as `public static final` values. +`Monoid` extends `Semigroup` because every monoid provides the associative `combine` operation and additionally supplies an identity value. -5. **Core stays independent.** - The core package has no dependency on examples and no external runtime dependency. +`Show` is intentionally independent from those hierarchies. -6. **Examples do not define architecture.** - `Person` exists to demonstrate the pattern, not as part of the reusable public abstraction. +## Explicit dictionary passing -## SOLID / KISS / DRY / YAGNI +A generic algorithm receives its behaviour as an argument: -### Single Responsibility Principle - -- `Show` defines one capability. -- instance holders define concrete behaviour. -- `Person` represents domain data. -- `TypeClassFunctions` hosts generic algorithms. - -### Open/Closed Principle - -New `Show` instances can be added without changing `A`, `Show`, or existing algorithms. - -### Liskov Substitution Principle - -Every `Show` implementation obeys the same minimal contract: it produces a string representation for an `A` according to its documented policy. - -### Interface Segregation Principle - -`Show` contains a single operation. Consumers are not forced to depend on unrelated capabilities. +```java +MonoidFunctions.combineAll(values, IntegerInstances.ADDITION); +``` -### Dependency Inversion Principle +No hidden global lookup occurs. This keeps dependencies local, testable, and visible. -Generic algorithms depend on the `Show` abstraction and receive concrete dictionaries from callers. +## Multiple instances -### KISS +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. -No automatic lookup mechanism is introduced. The central mechanism can be understood from one method call. +This is why instances remain external values rather than interfaces implemented by domain classes. -### DRY +## Immutability boundary -Generic rendering logic is expressed once and reused with different instances. +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. -### YAGNI +## SOLID -Abstractions planned for later milestones are not prematurely encoded in the core. +- **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. -## Null policy +## KISS, DRY, YAGNI -`Show` does not impose a global null policy. A particular instance may reject, support, or specially format null values. Generic infrastructure validates only its own required dependency: the `Show` dictionary itself. +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. -## Future evolution +## Compatibility decision from 0.1.1 -The architecture is intentionally prepared for additional first-order typeclasses without requiring a redesign: +`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. -```text -Show -Eq -Ord -Semigroup -Monoid -``` +## Deferred design questions -Higher-kinded encodings and automatic instance resolution are postponed until their dedicated milestones. +Milestone 0.3 will address executable algebraic laws. HKT representation, ADTs, instance resolution and derivation are intentionally deferred to later roadmap stages. diff --git a/docs/IMPLEMENTATION_TASKS.md b/docs/IMPLEMENTATION_TASKS.md index 641db9d..072e3fd 100644 --- a/docs/IMPLEMENTATION_TASKS.md +++ b/docs/IMPLEMENTATION_TASKS.md @@ -1,139 +1,131 @@ -# Milestone 0.1.1 — implementation tasks +# Milestone 0.2 — Detailed Implementation Tasks -## Task 1 — Freeze the 0.1 baseline +## Goal -1. Tag or branch the existing repository as milestone 0.1. -2. Record the current build command and source tree. -3. Keep the refactor behaviour-focused: remove legacy files only after confirming they are not referenced. +Turn the hardened 0.1.1 foundation into the first useful algebraic typeclass release without introducing higher-kinded types or hidden instance resolution. -**Done when:** the original state can be recovered and compared with 0.1.1. +## Task 1 — Freeze and verify the 0.1.1 baseline -## Task 2 — Clean repository metadata and build configuration +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. -1. Rename the Gradle root project from the generic `FunctionalJava` name to `java-typeclasses`. -2. Set `group` to `io.codeswarm` and version to `0.1.1`. -3. Replace the `java` plugin with `java-library`. -4. Configure the Java 21 toolchain. -5. Enable source and Javadoc JAR generation. -6. Upgrade the wrapper target to Gradle 9.8.0. -7. Replace the old JUnit BOM with JUnit 6.1.2. -8. Enable `-Xlint:all`, UTF-8, JaCoCo, and Javadoc verification. -9. Simplify `.gitignore` and remove obsolete generated/IDE files from version control. +## Task 2 — Version and build metadata -**Done when:** `clean`, `compileJava`, `test`, `jacocoTestReport`, and `javadoc` are defined by the build. +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 — Establish package boundaries +## Task 3 — Introduce `Eq` -1. Create `io.codeswarm.typeclasses.core` for reusable abstractions. -2. Create `io.codeswarm.typeclasses.instances` for reusable platform/JDK instances. -3. Create `io.codeswarm.typeclasses.examples` for educational models and demos. -4. Mirror the package structure in `src/test/java`. -5. Remove old default-package or legacy package source files once their behaviour is represented in the new structure. +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. -**Done when:** no production code remains in the default package and dependency direction is core <- instances/examples. +## Task 4 — Introduce `Ord` -## Task 4 — Implement the minimal typeclass contract +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. -1. Add `Show` as a `@FunctionalInterface`. -2. Document its role and its deliberately instance-specific null policy. -3. Do not add a marker `TypeClass` parent interface; it would provide no behaviour in this milestone. +## Task 5 — Introduce the algebra package -**Done when:** `Show` contains exactly one abstract operation and no dependency on examples. +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 5 — Implement explicit dictionary passing +## Task 6 — Refactor generic helper functions -1. Add `TypeClassFunctions` as a non-instantiable utility class. -2. Add `show(A, Show)`. -3. Validate only the required dictionary dependency, not the represented value. -4. Do not add reflection, registries, `ServiceLoader`, DI, annotations, or hidden lookup. +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. -**Done when:** generic rendering depends only on `Show` and a caller-supplied instance. +## Task 7 — Expand standard instances -## Task 6 — Add standard instances +### Integer -1. Add `StringInstances.SHOW`. -2. Add `IntegerInstances.SHOW`. -3. Add `LocalDateInstances.ISO_SHOW` to prove that third-party/JDK types can receive external behaviour. -4. Keep each holder focused on a single represented type. +Add `SHOW`, `EQ`, `ORD`, `ADDITION`, and `MULTIPLICATION`. -**Done when:** standard instances are stateless immutable values and contain no unrelated algorithms. +### String -## Task 7 — Add an immutable domain example +Add `SHOW`, `EQ`, `LEXICOGRAPHIC_ORD`, `CASE_INSENSITIVE_ORD`, and `CONCATENATION`. -1. Implement `Person` as a record. -2. Validate `name` and `age` in the canonical constructor. -3. Keep `Person` independent from `Show`. -4. Add `PersonInstances.COMPACT_SHOW`. -5. Add `PersonInstances.VERBOSE_SHOW`. +### LocalDate -**Done when:** two `Show` implementations coexist without changing `Person`. +Keep `ISO_SHOW` and add `EQ` and chronological `ORD`. -## Task 8 — Add an executable demonstration +### List -1. Add `ShowExample`. -2. Demonstrate compact and verbose person rendering. -3. Demonstrate integer rendering. -4. Demonstrate `LocalDate` rendering. -5. Keep the example deterministic and free of external I/O other than stdout. +Add a generic `concatenation()` monoid factory. Do not mutate input lists; return an immutable copy. -**Done when:** the example produces the four documented output lines. +## Task 8 — Expand the Person example -## Task 9 — Add unit tests +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. -1. Test generic delegation in `TypeClassFunctions`. -2. Test null-dictionary rejection. -3. Test instance-specific null handling. -4. Test each standard instance. -5. Test `Person` invariants. -6. Test both `Person` instances and their coexistence. +## Task 9 — Add executable examples -**Done when:** all public milestone behaviour has focused tests and examples are not used as substitutes for assertions. +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 — Add documentation +## Task 10 — Unit-test the complete milestone API -1. Replace the GitLab template README. -2. Add `ARCHITECTURE.md` with package boundaries, dependency direction, principles, and null policy. -3. Add `TYPECLASSES.md` explaining the Java encoding and dictionary passing. -4. Add `ROADMAP.md` covering 0.1 through 1.0. -5. Add `IMPLEMENTATION_TASKS.md`. -6. Add Javadoc to every public type and public member, plus useful documentation on test classes/methods for educational clarity. +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. -**Done when:** a new developer can understand the milestone without reading commit history. +## Task 11 — Javadoc -## Task 11 — Add CI +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. -1. Add a GitHub Actions workflow. -2. Test Java 21, 25, and 27. -3. Run `clean check javadoc`. -4. Keep workflow permissions read-only unless a future release job needs more. +## Task 12 — Documentation -**Done when:** pull requests verify compilation, tests, coverage report generation, and Javadoc. +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 12 — Record changes and remove obsolete files +## Task 13 — Cleanup -1. Add Apache-2.0 `LICENSE`. -2. Add `CHANGELOG.md` entry for 0.1.1. -3. Remove the placeholder GitLab README. -4. Remove unreferenced scripts and generated files from 0.1. -5. Remove legacy sources superseded by the new package structure. -6. Do not remove Gradle wrapper scripts/JAR when applying the milestone to the actual repository; regenerate them consistently for 9.8.0. +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. -**Done when:** repository root contains only build, source, documentation, CI, wrapper, and license files needed by the current project. +## Task 14 — Verification -## Task 13 — Final verification - -Run: - -```bash -gradle clean check javadoc -``` - -Then verify: - -- no compiler warnings introduced by project sources; -- all tests pass; -- JaCoCo HTML/XML reports exist; -- Javadoc is generated; -- no stale package/source files remain; -- README examples match actual APIs; -- version is exactly `0.1.1`. +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/ROADMAP.md b/docs/ROADMAP.md index 2dcf3f4..1aa4058 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,78 +1,73 @@ # Roadmap -The roadmap is incremental. Each milestone should remain buildable, documented, and understandable on its own. +## 0.1 — Original prototype -## 0.1 — Original proof of concept - -Initial experiments with typeclass-oriented functional programming in Java. +Initial experiment proving that the typeclass pattern can be represented in Java. ## 0.1.1 — Foundation hardening -- Java 21 baseline; -- modern Gradle build; -- clear package structure; -- explicit `Show` typeclass; +- modern build baseline; +- `Show`; - explicit dictionary passing; -- standard and domain-specific instances; -- multiple instances for one domain type; -- JUnit tests; -- JaCoCo and Javadoc; -- GitHub Actions CI; -- architecture, typeclass, roadmap, and changelog documentation. - -## 0.2 — Basic algebraic typeclasses +- external instances; +- clean package boundaries; +- tests, CI and documentation. -Planned: +## 0.2 — Basic algebraic typeclasses — current - `Eq`; - `Ord`; - `Semigroup`; - `Monoid`; -- standard instances for selected JDK types; -- reusable generic algorithms consuming those instances. +- multiple instances for the same type; +- immutable list concatenation; +- dedicated syntax helpers; +- algebra documentation. ## 0.3 — Typeclass laws Planned: -- reusable law definitions; -- property-based testing; -- associativity, identity, equality, and ordering laws; -- jqwik-based test support. +- reusable `EqLaws`, `OrdLaws`, `SemigroupLaws`, `MonoidLaws`; +- jqwik property-based testing; +- associativity, identity, reflexivity, symmetry, transitivity and ordering properties; +- lawful-instance test harness. ## 0.4 — Algebraic data types Planned: -- `Option`; -- `Either`; -- `Validated`; -- Java records, sealed interfaces, and pattern matching. +- `Option`; +- `Either`; +- `Validated`; +- sealed interfaces and records; +- typeclass instances for the ADTs. ## 0.5 — Higher-kinded type encoding Planned: -- `Kind` or equivalent witness encoding; -- safe inject/project boundaries; -- documentation of Java type-system limitations. +- `Kind`; +- witness types; +- safe conversion conventions; +- documentation of Java's HKT limitation. -## 0.6 — Functor / Applicative / Monad +## 0.6 — Functor, Applicative and Monad -Planned higher-order typeclasses based on the 0.5 encoding. +Planned after HKT encoding is established. -## 0.7 — Foldable / Traverse +## 0.7 — Foldable and Traverse -Planned structural typeclasses and instances for supported ADTs. +Planned generic folding and traversal abstractions. ## 0.8 — Instance resolution experiments -Planned investigation of explicit registries, type tokens, scoping, ambiguity detection, and a `summon`-style API. Explicit dictionary passing remains the reference semantics. +Possible explicit registry / `summon` experiments, kept outside the fundamental model. ## 0.9 — Automatic derivation -Planned annotation-processing or source-generation experiments for selected typeclasses such as `Eq` and `Show`. +Possible annotation-processing based derivation for selected typeclasses. ## 1.0 — Stable educational release -Planned stable public API, complete documentation, examples, compatibility policy, publishing metadata, and release automation. +A documented, law-tested API with a deliberate stability promise. diff --git a/docs/TYPECLASSES.md b/docs/TYPECLASSES.md index 2813d98..686267c 100644 --- a/docs/TYPECLASSES.md +++ b/docs/TYPECLASSES.md @@ -1,16 +1,12 @@ # Typeclasses in Java -## 1. Motivation +## Concept -A typeclass describes behaviour for a type without requiring that type to inherit from a particular interface. +A typeclass describes behaviour for a type without requiring that type to inherit from or implement the behaviour interface. -In milestone 0.1.1 the pattern has three pieces: +In this project a typeclass is represented by a small generic interface and an instance is an ordinary Java value implementing that interface. -1. a generic behavioural contract; -2. one or more instances of that contract for a concrete type; -3. generic algorithms that receive an instance explicitly. - -## 2. Contract +## Show ```java @FunctionalInterface @@ -19,61 +15,69 @@ public interface Show { } ``` -`Show` says only that values of `A` can be represented as text. It says nothing about how `A` is implemented. +`Person` does not implement `Show`. Instead, separate values provide compact and verbose representations. + +## Eq -## 3. Instance +`Eq` represents selectable equality semantics: ```java -Show decimalShow = String::valueOf; +Eq byName = + (left, right) -> left.name().equalsIgnoreCase(right.name()); ``` -The instance is an ordinary Java value. It can be stored in a constant, passed as an argument, selected dynamically, or created locally. +This differs from `Object.equals`: multiple valid equality policies can coexist and callers choose one explicitly. -## 4. Dictionary passing +## Ord -A typeclass instance can be understood as a dictionary containing the operations required by a generic algorithm. +`Ord` refines `Eq` with a comparison operation. Equality follows naturally when `compare(left, right) == 0`. -```java -static String show(A value, Show dictionary) { - return dictionary.show(value); -} -``` +A `Person` can therefore be ordered by age in one algorithm and by name in another without changing the record. -The dependency is explicit and compile-time typed. +## Semigroup -## 5. Multiple instances +A semigroup consists of a set of values and an associative `combine` operation. -Because behaviour is not attached to the model, several representations can coexist: +Examples include maximum over integers and string concatenation. -```java -Show compact = Person::name; -Show verbose = person -> - "%s (%d)".formatted(person.name(), person.age()); -``` +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 + +A monoid is a semigroup with an identity element. -The caller chooses the behaviour appropriate to its context. +Examples: -## 6. Existing types +| Type / operation | `combine` | identity | +| --- | --- | --- | +| Integer addition | `+` | `0` | +| Integer multiplication | `*` | `1` | +| String concatenation | concatenation | `""` | +| List concatenation | concatenation | `[]` | -Typeclasses can describe types that cannot be modified. The project defines an ISO rendering instance for `java.time.LocalDate` without subclassing or wrapping `LocalDate`. +## Explicit instance selection + +```java +var sum = MonoidFunctions.combineAll(values, IntegerInstances.ADDITION); +var product = MonoidFunctions.combineAll(values, IntegerInstances.MULTIPLICATION); +``` -## 7. Typeclass vs inheritance +The generic algorithm does not need to know anything about addition or multiplication. Behaviour is selected by supplying the appropriate dictionary value. -Inheritance is useful when a capability is an intrinsic part of a model's public contract. A typeclass-like approach is useful when behaviour should be: +## Typeclasses versus inheritance -- externally defined; -- independently evolvable; -- replaceable at the call site; -- available in several variants for one type. +With inheritance, a domain type usually embeds one implementation of an interface. Typeclasses invert that relationship: external instances describe the type. -The approaches are complementary rather than universally interchangeable. +This is especially useful when: -## 8. Typeclass vs Strategy +- 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. -In Java, the runtime mechanics are close to the Strategy pattern: behaviour is represented by an interface and passed to a consumer. The typeclass interpretation adds a type-oriented viewpoint: the interface represents an operation family parameterised by a target type, and generic algorithms are written in terms of that capability. +## Typeclasses versus Strategy -## 9. Java limitations +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. -Java has no native syntax corresponding to Scala `given`/`using`, Haskell typeclass instance search, or higher-kinded types. Milestone 0.1.1 intentionally does not hide those limitations. +## Deliberately missing in 0.2 -Later milestones may explore explicit encodings for higher-kinded types and instance resolution, but only after the basic first-order model is stable. +There is no global registry, implicit search, `summon`, reflection, annotations, automatic derivation, higher-kinded type encoding, `Functor`, `Applicative`, or `Monad` yet. diff --git a/gradle/wrapper/README.md b/gradle/wrapper/README.md index 09d070c..e4c7452 100644 --- a/gradle/wrapper/README.md +++ b/gradle/wrapper/README.md @@ -1,11 +1,11 @@ -# Gradle Wrapper bootstrap note +# Gradle Wrapper note -`gradle-wrapper.properties` targets Gradle 9.8.0. +The source archive contains the Gradle 9.8.0 wrapper configuration but does not fabricate a binary `gradle-wrapper.jar`. -The binary `gradle-wrapper.jar` is generated by Gradle and is intentionally not recreated from source in this archive. If it is not already present after applying this milestone on top of the repository, regenerate the wrapper once with: +If the wrapper JAR/scripts are absent in your checkout, regenerate them once with an installed Gradle distribution: ```bash gradle wrapper --gradle-version 9.8.0 ``` -In the original repository, keep the existing wrapper scripts/JAR when applying the refactor and regenerate them with the command above so all wrapper components are consistent. +Commit the generated wrapper JAR and scripts in the Git repository so normal consumers can use `./gradlew` without a local Gradle installation. diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/gradlew.bat b/gradlew.bat old mode 100644 new mode 100755 diff --git a/src/main/java/io/codeswarm/typeclasses/algebra/Monoid.java b/src/main/java/io/codeswarm/typeclasses/algebra/Monoid.java new file mode 100644 index 0000000..84311cc --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/algebra/Monoid.java @@ -0,0 +1,23 @@ +package io.codeswarm.typeclasses.algebra; + +/** + * Represents a {@link Semigroup} with an identity element. + * + *

In addition to associativity, a lawful monoid must satisfy left and right + * identity:

+ *
{@code
+ * combine(empty(), a) == a
+ * combine(a, empty()) == a
+ * }
+ * + * @param
the value type + */ +public interface Monoid extends Semigroup { + + /** + * Returns the identity element of this monoid. + * + * @return the identity value + */ + A empty(); +} diff --git a/src/main/java/io/codeswarm/typeclasses/algebra/Semigroup.java b/src/main/java/io/codeswarm/typeclasses/algebra/Semigroup.java new file mode 100644 index 0000000..92d682d --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/algebra/Semigroup.java @@ -0,0 +1,27 @@ +package io.codeswarm.typeclasses.algebra; + +/** + * Represents an associative binary operation over values of type {@code A}. + * + *

A lawful semigroup must satisfy associativity:

+ *
{@code
+ * 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 +public interface Semigroup { + + /** + * Combines two values. + * + * @param left the left value + * @param right the right value + * @return the combined value + */ + A combine(A left, A right); +} diff --git a/src/main/java/io/codeswarm/typeclasses/core/Eq.java b/src/main/java/io/codeswarm/typeclasses/core/Eq.java new file mode 100644 index 0000000..48ae975 --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/core/Eq.java @@ -0,0 +1,34 @@ +package io.codeswarm.typeclasses.core; + +/** + * Describes equality semantics for values of type {@code A}. + * + *

The equality strategy is independent of the domain type, so several + * meaningful definitions of equality may coexist for the same type.

+ * + * @param
the compared value type + */ +@FunctionalInterface +public interface Eq { + + /** + * Determines whether two values are equivalent according to this instance. + * + * @param left the left value + * @param right the right value + * @return {@code true} when the values are equivalent + */ + boolean eqv(A left, A right); + + /** + * Determines whether two values are not equivalent according to this + * instance. + * + * @param left the left value + * @param right the right value + * @return {@code true} when the values are not equivalent + */ + default boolean neqv(final A left, final A right) { + return !eqv(left, right); + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/core/Ord.java b/src/main/java/io/codeswarm/typeclasses/core/Ord.java new file mode 100644 index 0000000..14147f4 --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/core/Ord.java @@ -0,0 +1,79 @@ +package io.codeswarm.typeclasses.core; + +/** + * Defines a total ordering for values of type {@code A}. + * + *

An ordering is also an equality definition: values are equivalent when + * {@link #compare(Object, Object)} returns zero.

+ * + * @param
the ordered value type + */ +@FunctionalInterface +public interface Ord extends Eq { + + /** + * Compares two values. + * + * @param left the left value + * @param right the right value + * @return a negative number when {@code left < right}, zero when they are + * equivalent, or a positive number when {@code left > right} + */ + int compare(A left, A right); + + /** + * Determines equality from the ordering relation. + * + * @param left the left value + * @param right the right value + * @return {@code true} when {@link #compare(Object, Object)} returns zero + */ + @Override + default boolean eqv(final A left, final A right) { + return compare(left, right) == 0; + } + + /** + * Checks whether the left value precedes the right value. + * + * @param left the left value + * @param right the right value + * @return {@code true} when {@code left < right} + */ + default boolean lessThan(final A left, final A right) { + return compare(left, right) < 0; + } + + /** + * Checks whether the left value follows the right value. + * + * @param left the left value + * @param right the right value + * @return {@code true} when {@code left > right} + */ + default boolean greaterThan(final A left, final A right) { + return compare(left, right) > 0; + } + + /** + * Returns the smaller of two values according to this ordering. + * + * @param left the left value + * @param right the right value + * @return the smaller value; {@code left} is retained on equality + */ + default A min(final A left, final A right) { + return compare(left, right) <= 0 ? left : right; + } + + /** + * Returns the larger of two values according to this ordering. + * + * @param left the left value + * @param right the right value + * @return the larger value; {@code left} is retained on equality + */ + default A max(final A left, final A right) { + return compare(left, right) >= 0 ? left : right; + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/core/TypeClassFunctions.java b/src/main/java/io/codeswarm/typeclasses/core/TypeClassFunctions.java deleted file mode 100644 index b9d4ad8..0000000 --- a/src/main/java/io/codeswarm/typeclasses/core/TypeClassFunctions.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.codeswarm.typeclasses.core; - -import java.util.Objects; - -/** - * Contains generic algorithms that consume typeclass instances explicitly. - * - *

Milestone 0.1.1 intentionally uses explicit dictionary passing instead - * of reflection, a registry, dependency injection, or automatic instance - * resolution. This keeps the mechanism visible and makes dependencies easy to - * reason about.

- */ -public final class TypeClassFunctions { - - private TypeClassFunctions() { - throw new AssertionError("Utility class must not be instantiated"); - } - - /** - * Renders a value using the supplied {@link Show} instance. - * - * @param value the value to render - * @param show the typeclass instance that defines the rendering behavior - * @param
the value type - * @return the representation produced by {@code show} - * @throws NullPointerException if {@code show} is {@code null} - */ - public static String show(final A value, final Show show) { - Objects.requireNonNull(show, "show must not be null"); - return show.show(value); - } -} diff --git a/src/main/java/io/codeswarm/typeclasses/examples/EqExample.java b/src/main/java/io/codeswarm/typeclasses/examples/EqExample.java new file mode 100644 index 0000000..a09b1e7 --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/examples/EqExample.java @@ -0,0 +1,22 @@ +package io.codeswarm.typeclasses.examples; + +import io.codeswarm.typeclasses.syntax.EqFunctions; + +/** Demonstrates multiple equality definitions for the same domain type. */ +public final class EqExample { + + private EqExample() { + throw new AssertionError("Example class must not be instantiated"); + } + + /** + * Runs the equality example. + * + */ + 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)); + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/examples/MonoidExample.java b/src/main/java/io/codeswarm/typeclasses/examples/MonoidExample.java new file mode 100644 index 0000000..1ca724f --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/examples/MonoidExample.java @@ -0,0 +1,24 @@ +package io.codeswarm.typeclasses.examples; + +import io.codeswarm.typeclasses.instances.IntegerInstances; +import io.codeswarm.typeclasses.syntax.MonoidFunctions; + +import java.util.List; + +/** Demonstrates selecting different monoids for the same value type. */ +public final class MonoidExample { + + private MonoidExample() { + throw new AssertionError("Example class must not be instantiated"); + } + + /** + * Runs the monoid example. + * + */ + static void main() { + final var numbers = List.of(1, 2, 3, 4); + System.out.println(MonoidFunctions.combineAll(numbers, IntegerInstances.ADDITION)); + System.out.println(MonoidFunctions.combineAll(numbers, IntegerInstances.MULTIPLICATION)); + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/examples/OrdExample.java b/src/main/java/io/codeswarm/typeclasses/examples/OrdExample.java new file mode 100644 index 0000000..cd5ab17 --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/examples/OrdExample.java @@ -0,0 +1,22 @@ +package io.codeswarm.typeclasses.examples; + +import io.codeswarm.typeclasses.syntax.OrdFunctions; + +/** Demonstrates selecting an ordering independently of a domain type. */ +public final class OrdExample { + + private OrdExample() { + throw new AssertionError("Example class must not be instantiated"); + } + + /** + * Runs the ordering example. + * + */ + 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)); + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/examples/Person.java b/src/main/java/io/codeswarm/typeclasses/examples/Person.java index b7928e8..08b5f7f 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/Person.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/Person.java @@ -3,7 +3,7 @@ import java.util.Objects; /** - * Example immutable domain value used to demonstrate that a model does not + * Immutable example domain value used to demonstrate that a model does not * need to implement its typeclasses. * * @param name the person's non-null name diff --git a/src/main/java/io/codeswarm/typeclasses/examples/PersonInstances.java b/src/main/java/io/codeswarm/typeclasses/examples/PersonInstances.java index 6158f5f..4a91d90 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/PersonInstances.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/PersonInstances.java @@ -1,27 +1,41 @@ package io.codeswarm.typeclasses.examples; +import io.codeswarm.typeclasses.core.Eq; +import io.codeswarm.typeclasses.core.Ord; import io.codeswarm.typeclasses.core.Show; /** * Typeclass instances associated with the example {@link Person} domain type. * - *

The instances are deliberately kept outside {@code Person}. This makes - * the domain model independent of presentation concerns and permits several - * valid representations to coexist.

+ *

The instances are deliberately kept outside {@code Person}. This keeps + * the domain model independent of equality, ordering, and presentation policy + * and permits several valid interpretations to coexist.

*/ public final class PersonInstances { - /** - * Compact representation containing only the person's name. - */ + /** Compact representation containing only the person's name. */ public static final Show COMPACT_SHOW = Person::name; - /** - * Verbose representation containing both the person's name and age. - */ + /** Verbose representation containing both the person's name and age. */ public static final Show VERBOSE_SHOW = person -> "%s (%d)".formatted(person.name(), person.age()); + /** Equality based on both record components. */ + public static final Eq BY_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 = + (left, right) -> left.name().equalsIgnoreCase(right.name()); + + /** Orders people by age. */ + public static final Ord BY_AGE = + (left, right) -> Integer.compare(left.age(), right.age()); + + /** Orders people case-insensitively by name. */ + public static final Ord BY_NAME_ORDER = + (left, right) -> left.name().compareToIgnoreCase(right.name()); + private PersonInstances() { throw new AssertionError("Instances holder must not be instantiated"); } diff --git a/src/main/java/io/codeswarm/typeclasses/examples/SemigroupExample.java b/src/main/java/io/codeswarm/typeclasses/examples/SemigroupExample.java new file mode 100644 index 0000000..31594e4 --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/examples/SemigroupExample.java @@ -0,0 +1,23 @@ +package io.codeswarm.typeclasses.examples; + +import io.codeswarm.typeclasses.algebra.Semigroup; +import io.codeswarm.typeclasses.syntax.SemigroupFunctions; + +import java.util.List; + +/** Demonstrates a semigroup and its non-empty fold semantics. */ +public final class SemigroupExample { + + private SemigroupExample() { + throw new AssertionError("Example class must not be instantiated"); + } + + /** + * Runs the semigroup example. + * + */ + static void main() { + final Semigroup maximum = Integer::max; + System.out.println(SemigroupFunctions.combineAll(List.of(4, 9, 2, 7), maximum)); + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/examples/ShowExample.java b/src/main/java/io/codeswarm/typeclasses/examples/ShowExample.java index 60dd9b1..9e449d5 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/ShowExample.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/ShowExample.java @@ -1,14 +1,12 @@ package io.codeswarm.typeclasses.examples; -import io.codeswarm.typeclasses.core.TypeClassFunctions; import io.codeswarm.typeclasses.instances.IntegerInstances; import io.codeswarm.typeclasses.instances.LocalDateInstances; +import io.codeswarm.typeclasses.syntax.ShowFunctions; import java.time.LocalDate; -/** - * Small executable demonstration of explicit typeclass instance passing. - */ +/** Demonstrates explicit use of {@code Show} instances. */ public final class ShowExample { private ShowExample() { @@ -16,17 +14,15 @@ private ShowExample() { } /** - * Runs the milestone 0.1.1 example. + * Runs the show example. * */ static void main() { final var person = new Person("Alice", 30); - - System.out.println(TypeClassFunctions.show(person, PersonInstances.COMPACT_SHOW)); - System.out.println(TypeClassFunctions.show(person, PersonInstances.VERBOSE_SHOW)); - System.out.println(TypeClassFunctions.show(42, IntegerInstances.SHOW)); - System.out.println(TypeClassFunctions.show( - LocalDate.of(2026, 9, 30), - LocalDateInstances.ISO_SHOW)); + System.out.println(ShowFunctions.show(person, PersonInstances.COMPACT_SHOW)); + System.out.println(ShowFunctions.show(person, PersonInstances.VERBOSE_SHOW)); + 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/main/java/io/codeswarm/typeclasses/instances/IntegerInstances.java b/src/main/java/io/codeswarm/typeclasses/instances/IntegerInstances.java index 07e0566..668d3bb 100644 --- a/src/main/java/io/codeswarm/typeclasses/instances/IntegerInstances.java +++ b/src/main/java/io/codeswarm/typeclasses/instances/IntegerInstances.java @@ -1,5 +1,8 @@ package io.codeswarm.typeclasses.instances; +import io.codeswarm.typeclasses.algebra.Monoid; +import io.codeswarm.typeclasses.core.Eq; +import io.codeswarm.typeclasses.core.Ord; import io.codeswarm.typeclasses.core.Show; /** @@ -7,10 +10,48 @@ */ public final class IntegerInstances { + /** Renders an integer using its standard decimal representation. */ + public static final Show SHOW = String::valueOf; + + /** Compares integers using value equality. */ + public static final Eq EQ = Integer::equals; + + /** Orders integers by their numeric value. */ + public static final Ord ORD = Integer::compare; + /** - * Renders an integer using its standard decimal representation. + * Integer monoid under addition. The identity element is {@code 0}. */ - public static final Show SHOW = String::valueOf; + public static final Monoid ADDITION = new Monoid<>() { + /** {@inheritDoc} */ + @Override + public Integer combine(final Integer left, final Integer right) { + return left + right; + } + + /** {@inheritDoc} */ + @Override + public Integer empty() { + return 0; + } + }; + + /** + * Integer monoid under multiplication. The identity element is {@code 1}. + */ + public static final Monoid MULTIPLICATION = new Monoid<>() { + /** {@inheritDoc} */ + @Override + public Integer combine(final Integer left, final Integer right) { + return left * right; + } + + /** {@inheritDoc} */ + @Override + public Integer empty() { + return 1; + } + }; private IntegerInstances() { throw new AssertionError("Instances holder must not be instantiated"); diff --git a/src/main/java/io/codeswarm/typeclasses/instances/ListInstances.java b/src/main/java/io/codeswarm/typeclasses/instances/ListInstances.java new file mode 100644 index 0000000..fba7be6 --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/instances/ListInstances.java @@ -0,0 +1,46 @@ +package io.codeswarm.typeclasses.instances; + +import io.codeswarm.typeclasses.algebra.Monoid; + +import java.util.ArrayList; +import java.util.List; + +/** + * Typeclass instance factories for immutable-style operations on {@link List} + * values. + */ +public final class ListInstances { + + private ListInstances() { + throw new AssertionError("Instances holder must not be instantiated"); + } + + /** + * Creates a list monoid under concatenation. + * + *

The implementation never mutates either input list. Every non-trivial + * combination returns an unmodifiable copy and the identity is + * {@link List#of()}.

+ * + * @param
the list element type + * @return a monoid that concatenates lists + */ + public static Monoid> concatenation() { + return new Monoid<>() { + /** {@inheritDoc} */ + @Override + public List combine(final List left, final List right) { + final var result = new ArrayList(left.size() + right.size()); + result.addAll(left); + result.addAll(right); + return List.copyOf(result); + } + + /** {@inheritDoc} */ + @Override + public List empty() { + return List.of(); + } + }; + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/instances/LocalDateInstances.java b/src/main/java/io/codeswarm/typeclasses/instances/LocalDateInstances.java index fc9e25c..1d361f5 100644 --- a/src/main/java/io/codeswarm/typeclasses/instances/LocalDateInstances.java +++ b/src/main/java/io/codeswarm/typeclasses/instances/LocalDateInstances.java @@ -1,5 +1,7 @@ package io.codeswarm.typeclasses.instances; +import io.codeswarm.typeclasses.core.Eq; +import io.codeswarm.typeclasses.core.Ord; import io.codeswarm.typeclasses.core.Show; import java.time.LocalDate; @@ -8,19 +10,21 @@ /** * Standard typeclass instances for {@link LocalDate} values. * - *

This class also demonstrates an important advantage of the typeclass - * pattern: behavior can be added to a type from the JDK without modifying or - * extending that type.

+ *

This class demonstrates that typeclass behavior can be supplied for a + * JDK type without modifying or extending that type.

*/ public final class LocalDateInstances { - /** - * Renders a date in ISO-8601 local-date format, for example - * {@code 2026-09-30}. - */ + /** Renders dates in ISO-8601 local-date format. */ public static final Show ISO_SHOW = DateTimeFormatter.ISO_LOCAL_DATE::format; + /** Compares dates using standard value equality. */ + public static final Eq EQ = LocalDate::equals; + + /** Orders dates chronologically. */ + public static final Ord ORD = LocalDate::compareTo; + private LocalDateInstances() { throw new AssertionError("Instances holder must not be instantiated"); } diff --git a/src/main/java/io/codeswarm/typeclasses/instances/StringInstances.java b/src/main/java/io/codeswarm/typeclasses/instances/StringInstances.java index 5fd4ad1..63590bb 100644 --- a/src/main/java/io/codeswarm/typeclasses/instances/StringInstances.java +++ b/src/main/java/io/codeswarm/typeclasses/instances/StringInstances.java @@ -1,5 +1,8 @@ package io.codeswarm.typeclasses.instances; +import io.codeswarm.typeclasses.algebra.Monoid; +import io.codeswarm.typeclasses.core.Eq; +import io.codeswarm.typeclasses.core.Ord; import io.codeswarm.typeclasses.core.Show; /** @@ -7,11 +10,33 @@ */ public final class StringInstances { - /** - * Renders a string without adding formatting or quotation marks. - */ + /** Renders a string without additional formatting. */ public static final Show SHOW = value -> value; + /** Compares strings using standard case-sensitive equality. */ + public static final Eq EQ = String::equals; + + /** Orders strings lexicographically using their natural ordering. */ + public static final Ord LEXICOGRAPHIC_ORD = String::compareTo; + + /** Orders strings lexicographically while ignoring case. */ + public static final Ord CASE_INSENSITIVE_ORD = String::compareToIgnoreCase; + + /** String monoid under concatenation. The identity is the empty string. */ + public static final Monoid CONCATENATION = new Monoid<>() { + /** {@inheritDoc} */ + @Override + public String combine(final String left, final String right) { + return left + right; + } + + /** {@inheritDoc} */ + @Override + public String empty() { + return ""; + } + }; + private StringInstances() { throw new AssertionError("Instances holder must not be instantiated"); } diff --git a/src/main/java/io/codeswarm/typeclasses/syntax/EqFunctions.java b/src/main/java/io/codeswarm/typeclasses/syntax/EqFunctions.java new file mode 100644 index 0000000..3e952be --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/syntax/EqFunctions.java @@ -0,0 +1,33 @@ +package io.codeswarm.typeclasses.syntax; + +import io.codeswarm.typeclasses.core.Eq; + +import java.util.Objects; + +/** + * Generic operations that consume {@link Eq} instances explicitly. + */ +public final class EqFunctions { + + private EqFunctions() { + throw new AssertionError("Utility class must not be instantiated"); + } + + /** + * Compares two values using the supplied equality definition. + * + * @param left the left value + * @param right the right value + * @param instance the equality instance + * @param
the compared value type + * @return {@code true} when both values are equivalent + * @throws NullPointerException if {@code instance} is {@code null} + */ + public static boolean equal( + final A left, + final A right, + final Eq instance) { + Objects.requireNonNull(instance, "instance must not be null"); + return instance.eqv(left, right); + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/syntax/MonoidFunctions.java b/src/main/java/io/codeswarm/typeclasses/syntax/MonoidFunctions.java new file mode 100644 index 0000000..278ea1d --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/syntax/MonoidFunctions.java @@ -0,0 +1,40 @@ +package io.codeswarm.typeclasses.syntax; + +import io.codeswarm.typeclasses.algebra.Monoid; + +import java.util.Objects; + +/** + * Generic algorithms based on {@link Monoid}. + */ +public final class MonoidFunctions { + + private MonoidFunctions() { + throw new AssertionError("Utility class must not be instantiated"); + } + + /** + * Combines all values from left to right, starting with the monoid identity. + * + *

Unlike a semigroup fold, this operation is defined for an empty + * iterable and returns {@link Monoid#empty()} in that case.

+ * + * @param values the values to combine + * @param instance the monoid instance + * @param
the value type + * @return the combined value, or the identity for an empty iterable + * @throws NullPointerException if {@code values} or {@code instance} is null + */ + public static A combineAll( + final Iterable values, + final Monoid instance) { + Objects.requireNonNull(values, "values must not be null"); + Objects.requireNonNull(instance, "instance must not be null"); + + A result = instance.empty(); + for (final A value : values) { + result = instance.combine(result, value); + } + return result; + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/syntax/OrdFunctions.java b/src/main/java/io/codeswarm/typeclasses/syntax/OrdFunctions.java new file mode 100644 index 0000000..ed8b551 --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/syntax/OrdFunctions.java @@ -0,0 +1,45 @@ +package io.codeswarm.typeclasses.syntax; + +import io.codeswarm.typeclasses.core.Ord; + +import java.util.Objects; + +/** + * Generic operations that consume {@link Ord} instances explicitly. + */ +public final class OrdFunctions { + + private OrdFunctions() { + throw new AssertionError("Utility class must not be instantiated"); + } + + /** + * Returns the smaller of two values according to the supplied ordering. + * + * @param left the left value + * @param right the right value + * @param instance the ordering instance + * @param the ordered value type + * @return the smaller value + * @throws NullPointerException if {@code instance} is {@code null} + */ + public static A min(final A left, final A right, final Ord instance) { + Objects.requireNonNull(instance, "instance must not be null"); + return instance.min(left, right); + } + + /** + * Returns the larger of two values according to the supplied ordering. + * + * @param left the left value + * @param right the right value + * @param instance the ordering instance + * @param the ordered value type + * @return the larger value + * @throws NullPointerException if {@code instance} is {@code null} + */ + public static A max(final A left, final A right, final Ord instance) { + Objects.requireNonNull(instance, "instance must not be null"); + return instance.max(left, right); + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/syntax/SemigroupFunctions.java b/src/main/java/io/codeswarm/typeclasses/syntax/SemigroupFunctions.java new file mode 100644 index 0000000..5f6b061 --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/syntax/SemigroupFunctions.java @@ -0,0 +1,48 @@ +package io.codeswarm.typeclasses.syntax; + +import io.codeswarm.typeclasses.algebra.Semigroup; + +import java.util.Iterator; +import java.util.Objects; + +/** + * Generic algorithms based on {@link Semigroup}. + */ +public final class SemigroupFunctions { + + private SemigroupFunctions() { + throw new AssertionError("Utility class must not be instantiated"); + } + + /** + * Combines all values from a non-empty iterable from left to right. + * + *

A semigroup has no identity element, therefore there is no lawful + * result for an empty iterable.

+ * + * @param values the values to combine + * @param instance the semigroup instance + * @param
the value type + * @return the combined value + * @throws NullPointerException if {@code values} or {@code instance} is null + * @throws IllegalArgumentException if {@code values} is empty + */ + public static A combineAll( + final Iterable values, + final Semigroup instance) { + Objects.requireNonNull(values, "values must not be null"); + Objects.requireNonNull(instance, "instance must not be null"); + + final Iterator iterator = values.iterator(); + if (!iterator.hasNext()) { + throw new IllegalArgumentException( + "Semigroup cannot combine an empty iterable"); + } + + A result = iterator.next(); + while (iterator.hasNext()) { + result = instance.combine(result, iterator.next()); + } + return result; + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/syntax/ShowFunctions.java b/src/main/java/io/codeswarm/typeclasses/syntax/ShowFunctions.java new file mode 100644 index 0000000..00d35af --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/syntax/ShowFunctions.java @@ -0,0 +1,29 @@ +package io.codeswarm.typeclasses.syntax; + +import io.codeswarm.typeclasses.core.Show; + +import java.util.Objects; + +/** + * Generic operations that consume {@link Show} instances explicitly. + */ +public final class ShowFunctions { + + private ShowFunctions() { + throw new AssertionError("Utility class must not be instantiated"); + } + + /** + * Renders a value with the supplied {@link Show} instance. + * + * @param value the value to render + * @param instance the rendering instance + * @param the value type + * @return the rendered value + * @throws NullPointerException if {@code instance} is {@code null} + */ + public static String show(final A value, final Show instance) { + Objects.requireNonNull(instance, "instance must not be null"); + return instance.show(value); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/algebra/MonoidTest.java b/src/test/java/io/codeswarm/typeclasses/algebra/MonoidTest.java new file mode 100644 index 0000000..a54fd67 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/algebra/MonoidTest.java @@ -0,0 +1,17 @@ +package io.codeswarm.typeclasses.algebra; + +import io.codeswarm.typeclasses.instances.IntegerInstances; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** Unit tests for representative {@link Monoid} instances. */ +class MonoidTest { + + /** Verifies that different monoids over Integer expose different identities. */ + @Test + void shouldExposeOperationSpecificIdentity() { + assertEquals(0, IntegerInstances.ADDITION.empty()); + assertEquals(1, IntegerInstances.MULTIPLICATION.empty()); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/algebra/SemigroupTest.java b/src/test/java/io/codeswarm/typeclasses/algebra/SemigroupTest.java new file mode 100644 index 0000000..e5b62ca --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/algebra/SemigroupTest.java @@ -0,0 +1,16 @@ +package io.codeswarm.typeclasses.algebra; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** Unit tests for a representative {@link Semigroup} instance. */ +class SemigroupTest { + + /** Verifies that a semigroup can model a standalone associative combination. */ + @Test + void shouldCombineValues() { + final Semigroup maximum = Integer::max; + assertEquals(20, maximum.combine(10, 20)); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/core/EqTest.java b/src/test/java/io/codeswarm/typeclasses/core/EqTest.java new file mode 100644 index 0000000..25cf572 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/core/EqTest.java @@ -0,0 +1,20 @@ +package io.codeswarm.typeclasses.core; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Unit tests for {@link Eq}. */ +class EqTest { + + /** Verifies the primary equality operation and its negated convenience method. */ + @Test + void shouldCompareValuesAndNegateEquality() { + final Eq eq = Integer::equals; + assertTrue(eq.eqv(42, 42)); + assertFalse(eq.eqv(42, 43)); + assertTrue(eq.neqv(42, 43)); + assertFalse(eq.neqv(42, 42)); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/core/OrdTest.java b/src/test/java/io/codeswarm/typeclasses/core/OrdTest.java new file mode 100644 index 0000000..24106e8 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/core/OrdTest.java @@ -0,0 +1,24 @@ +package io.codeswarm.typeclasses.core; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Unit tests for {@link Ord} default operations. */ +class OrdTest { + + private static final Ord ORD = Integer::compare; + + /** Verifies equality, comparison predicates, minimum, and maximum helpers. */ + @Test + void shouldExposeOrderingOperations() { + assertTrue(ORD.eqv(10, 10)); + assertTrue(ORD.lessThan(10, 20)); + assertTrue(ORD.greaterThan(20, 10)); + assertFalse(ORD.lessThan(20, 10)); + assertEquals(10, ORD.min(10, 20)); + assertEquals(20, ORD.max(10, 20)); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/core/TypeClassFunctionsTest.java b/src/test/java/io/codeswarm/typeclasses/core/TypeClassFunctionsTest.java deleted file mode 100644 index 89eadfe..0000000 --- a/src/test/java/io/codeswarm/typeclasses/core/TypeClassFunctionsTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.codeswarm.typeclasses.core; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -/** - * Tests for {@link TypeClassFunctions}. - */ -class TypeClassFunctionsTest { - - /** - * Verifies that a generic algorithm delegates rendering to the supplied - * dictionary rather than relying on the runtime type. - */ - @Test - void shouldUseProvidedShowInstance() { - final Show hexadecimal = Integer::toHexString; - - assertEquals("ff", TypeClassFunctions.show(255, hexadecimal)); - } - - /** - * Verifies that a missing typeclass dictionary fails immediately with a - * clear error rather than producing a later, less useful failure. - */ - @Test - void shouldRejectNullShowInstance() { - assertThrows( - NullPointerException.class, - () -> TypeClassFunctions.show(1, null)); - } - - /** - * Verifies that null handling remains the responsibility of an individual - * typeclass instance. - */ - @Test - void shouldAllowInstanceSpecificNullPolicy() { - final Show nullAware = value -> value == null ? "" : value; - - assertEquals("", TypeClassFunctions.show(null, nullAware)); - } -} diff --git a/src/test/java/io/codeswarm/typeclasses/examples/PersonInstancesTest.java b/src/test/java/io/codeswarm/typeclasses/examples/PersonInstancesTest.java index 2ae9d82..c91f05a 100644 --- a/src/test/java/io/codeswarm/typeclasses/examples/PersonInstancesTest.java +++ b/src/test/java/io/codeswarm/typeclasses/examples/PersonInstancesTest.java @@ -3,42 +3,35 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; -/** - * Tests demonstrating that several typeclass instances may coexist for one - * domain type. - */ +/** Unit tests for {@link PersonInstances}. */ class PersonInstancesTest { - /** - * Verifies the compact person representation. - */ + /** Verifies multiple renderings for the same domain value. */ @Test - void shouldUseCompactRepresentation() { + void shouldSupportMultipleShowInstances() { final var person = new Person("Alice", 30); - assertEquals("Alice", PersonInstances.COMPACT_SHOW.show(person)); + assertEquals("Alice (30)", PersonInstances.VERBOSE_SHOW.show(person)); } - /** - * Verifies the verbose person representation. - */ + /** Verifies that equality policy can be selected independently of Person. */ @Test - void shouldUseVerboseRepresentation() { - final var person = new Person("Alice", 30); - - assertEquals("Alice (30)", PersonInstances.VERBOSE_SHOW.show(person)); + 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)); } - /** - * Demonstrates that choosing behaviour is a call-site decision rather than - * a responsibility permanently attached to the domain class. - */ + /** Verifies that ordering policy can be selected independently of Person. */ @Test - void shouldAllowMultipleInstancesForSameType() { - final var person = new Person("Alice", 30); - - assertEquals("Alice", PersonInstances.COMPACT_SHOW.show(person)); - assertEquals("Alice (30)", PersonInstances.VERBOSE_SHOW.show(person)); + 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)); } } diff --git a/src/test/java/io/codeswarm/typeclasses/examples/PersonTest.java b/src/test/java/io/codeswarm/typeclasses/examples/PersonTest.java index ac2c5c5..ad868c2 100644 --- a/src/test/java/io/codeswarm/typeclasses/examples/PersonTest.java +++ b/src/test/java/io/codeswarm/typeclasses/examples/PersonTest.java @@ -5,36 +5,26 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -/** - * Tests for the example {@link Person} domain value. - */ +/** Unit tests for {@link Person}. */ class PersonTest { - /** - * Verifies that valid data is retained by the record. - */ + /** Verifies construction of a valid person. */ @Test void shouldCreateValidPerson() { final var person = new Person("Alice", 30); - assertEquals("Alice", person.name()); assertEquals(30, person.age()); } - /** - * Verifies that invalid negative age values are rejected at the domain - * boundary. - */ + /** Verifies name validation. */ @Test - void shouldRejectNegativeAge() { - assertThrows(IllegalArgumentException.class, () -> new Person("Alice", -1)); + void shouldRejectNullName() { + assertThrows(NullPointerException.class, () -> new Person(null, 30)); } - /** - * Verifies that a person always has a name. - */ + /** Verifies age validation. */ @Test - void shouldRejectNullName() { - assertThrows(NullPointerException.class, () -> new Person(null, 30)); + void shouldRejectNegativeAge() { + assertThrows(IllegalArgumentException.class, () -> new Person("Alice", -1)); } } diff --git a/src/test/java/io/codeswarm/typeclasses/instances/IntegerInstancesTest.java b/src/test/java/io/codeswarm/typeclasses/instances/IntegerInstancesTest.java new file mode 100644 index 0000000..daf1ff7 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/instances/IntegerInstancesTest.java @@ -0,0 +1,20 @@ +package io.codeswarm.typeclasses.instances; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Unit tests for {@link IntegerInstances}. */ +class IntegerInstancesTest { + + /** Verifies show, equality, ordering, addition, and multiplication instances. */ + @Test + void shouldProvideIntegerInstances() { + assertEquals("42", IntegerInstances.SHOW.show(42)); + assertTrue(IntegerInstances.EQ.eqv(42, 42)); + assertTrue(IntegerInstances.ORD.lessThan(1, 2)); + assertEquals(3, IntegerInstances.ADDITION.combine(1, 2)); + assertEquals(6, IntegerInstances.MULTIPLICATION.combine(2, 3)); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/instances/ListInstancesTest.java b/src/test/java/io/codeswarm/typeclasses/instances/ListInstancesTest.java new file mode 100644 index 0000000..6c1b63c --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/instances/ListInstancesTest.java @@ -0,0 +1,31 @@ +package io.codeswarm.typeclasses.instances; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** Unit tests for {@link ListInstances}. */ +class ListInstancesTest { + + /** Verifies concatenation and that input lists are not mutated. */ + @Test + void shouldConcatenateWithoutMutatingInputs() { + final var left = new ArrayList<>(List.of(1, 2)); + final var right = new ArrayList<>(List.of(3, 4)); + final var result = ListInstances.concatenation().combine(left, right); + assertEquals(List.of(1, 2, 3, 4), result); + assertEquals(List.of(1, 2), left); + assertEquals(List.of(3, 4), right); + assertThrows(UnsupportedOperationException.class, () -> result.add(5)); + } + + /** Verifies that the empty immutable list is the identity. */ + @Test + void shouldProvideEmptyListIdentity() { + assertEquals(List.of(), ListInstances.concatenation().empty()); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/instances/LocalDateInstancesTest.java b/src/test/java/io/codeswarm/typeclasses/instances/LocalDateInstancesTest.java new file mode 100644 index 0000000..fafa5c7 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/instances/LocalDateInstancesTest.java @@ -0,0 +1,22 @@ +package io.codeswarm.typeclasses.instances; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Unit tests for {@link LocalDateInstances}. */ +class LocalDateInstancesTest { + + /** Verifies rendering, equality, and chronological ordering. */ + @Test + void shouldProvideLocalDateInstances() { + final var first = LocalDate.of(2026, 9, 30); + final var second = LocalDate.of(2026, 10, 1); + assertEquals("2026-09-30", LocalDateInstances.ISO_SHOW.show(first)); + assertTrue(LocalDateInstances.EQ.eqv(first, first)); + assertTrue(LocalDateInstances.ORD.lessThan(first, second)); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/instances/StringInstancesTest.java b/src/test/java/io/codeswarm/typeclasses/instances/StringInstancesTest.java new file mode 100644 index 0000000..380d4b1 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/instances/StringInstancesTest.java @@ -0,0 +1,21 @@ +package io.codeswarm.typeclasses.instances; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Unit tests for {@link StringInstances}. */ +class StringInstancesTest { + + /** Verifies string rendering, equality, ordering, and concatenation. */ + @Test + void shouldProvideStringInstances() { + assertEquals("abc", StringInstances.SHOW.show("abc")); + assertTrue(StringInstances.EQ.eqv("abc", "abc")); + assertTrue(StringInstances.LEXICOGRAPHIC_ORD.lessThan("a", "b")); + assertTrue(StringInstances.CASE_INSENSITIVE_ORD.eqv("ABC", "abc")); + assertEquals("ab", StringInstances.CONCATENATION.combine("a", "b")); + assertEquals("", StringInstances.CONCATENATION.empty()); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/syntax/EqFunctionsTest.java b/src/test/java/io/codeswarm/typeclasses/syntax/EqFunctionsTest.java new file mode 100644 index 0000000..1309dd1 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/syntax/EqFunctionsTest.java @@ -0,0 +1,23 @@ +package io.codeswarm.typeclasses.syntax; + +import io.codeswarm.typeclasses.instances.IntegerInstances; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Unit tests for {@link EqFunctions}. */ +class EqFunctionsTest { + + /** Verifies explicit dictionary passing for Eq. */ + @Test + void shouldCompareUsingProvidedInstance() { + assertTrue(EqFunctions.equal(42, 42, IntegerInstances.EQ)); + } + + /** Verifies that the required instance dependency is validated. */ + @Test + void shouldRejectNullInstance() { + assertThrows(NullPointerException.class, () -> EqFunctions.equal(1, 1, null)); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/syntax/MonoidFunctionsTest.java b/src/test/java/io/codeswarm/typeclasses/syntax/MonoidFunctionsTest.java new file mode 100644 index 0000000..67bea7b --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/syntax/MonoidFunctionsTest.java @@ -0,0 +1,27 @@ +package io.codeswarm.typeclasses.syntax; + +import io.codeswarm.typeclasses.instances.IntegerInstances; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** Unit tests for {@link MonoidFunctions}. */ +class MonoidFunctionsTest { + + /** Verifies that changing only the monoid changes the generic fold semantics. */ + @Test + void shouldUseSelectedMonoid() { + final var values = List.of(1, 2, 3, 4); + assertEquals(10, MonoidFunctions.combineAll(values, IntegerInstances.ADDITION)); + assertEquals(24, MonoidFunctions.combineAll(values, IntegerInstances.MULTIPLICATION)); + } + + /** Verifies that an empty iterable evaluates to the selected identity. */ + @Test + void shouldReturnIdentityForEmptyIterable() { + assertEquals(0, MonoidFunctions.combineAll(List.of(), IntegerInstances.ADDITION)); + assertEquals(1, MonoidFunctions.combineAll(List.of(), IntegerInstances.MULTIPLICATION)); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/syntax/OrdFunctionsTest.java b/src/test/java/io/codeswarm/typeclasses/syntax/OrdFunctionsTest.java new file mode 100644 index 0000000..afb4821 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/syntax/OrdFunctionsTest.java @@ -0,0 +1,24 @@ +package io.codeswarm.typeclasses.syntax; + +import io.codeswarm.typeclasses.instances.IntegerInstances; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** Unit tests for {@link OrdFunctions}. */ +class OrdFunctionsTest { + + /** Verifies generic minimum and maximum algorithms. */ + @Test + void shouldSelectMinimumAndMaximum() { + assertEquals(10, OrdFunctions.min(10, 20, IntegerInstances.ORD)); + assertEquals(20, OrdFunctions.max(10, 20, IntegerInstances.ORD)); + } + + /** Verifies that the required instance dependency is validated. */ + @Test + void shouldRejectNullInstance() { + assertThrows(NullPointerException.class, () -> OrdFunctions.min(1, 2, null)); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/syntax/SemigroupFunctionsTest.java b/src/test/java/io/codeswarm/typeclasses/syntax/SemigroupFunctionsTest.java new file mode 100644 index 0000000..f2ae092 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/syntax/SemigroupFunctionsTest.java @@ -0,0 +1,28 @@ +package io.codeswarm.typeclasses.syntax; + +import io.codeswarm.typeclasses.algebra.Semigroup; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** Unit tests for {@link SemigroupFunctions}. */ +class SemigroupFunctionsTest { + + private static final Semigroup MAXIMUM = Integer::max; + + /** Verifies left-to-right combination of a non-empty iterable. */ + @Test + void shouldCombineNonEmptyIterable() { + assertEquals(9, SemigroupFunctions.combineAll(List.of(4, 9, 2), MAXIMUM)); + } + + /** Verifies that an empty iterable has no semigroup result. */ + @Test + void shouldRejectEmptyIterable() { + assertThrows(IllegalArgumentException.class, + () -> SemigroupFunctions.combineAll(List.of(), MAXIMUM)); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/syntax/ShowFunctionsTest.java b/src/test/java/io/codeswarm/typeclasses/syntax/ShowFunctionsTest.java new file mode 100644 index 0000000..c610db4 --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/syntax/ShowFunctionsTest.java @@ -0,0 +1,23 @@ +package io.codeswarm.typeclasses.syntax; + +import io.codeswarm.typeclasses.instances.IntegerInstances; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** Unit tests for {@link ShowFunctions}. */ +class ShowFunctionsTest { + + /** Verifies explicit dictionary passing for Show. */ + @Test + void shouldRenderUsingProvidedInstance() { + assertEquals("42", ShowFunctions.show(42, IntegerInstances.SHOW)); + } + + /** Verifies that the required instance dependency is validated. */ + @Test + void shouldRejectNullInstance() { + assertThrows(NullPointerException.class, () -> ShowFunctions.show(42, null)); + } +}