diff --git a/CHANGELOG.md b/CHANGELOG.md index a574257..e3f61b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,42 @@ All notable changes to this educational project are documented here. +## 0.5.0 + +### Added + +- minimal higher-kinded type encoding `Kind`, +- documented `hkt` package, +- `OptionK`, `EitherK` and `ValidatedK` witness types, +- `OptionKinds`, `EitherKinds` and `ValidatedKinds`, +- centralized `widen` / `narrow` conversions, +- HKT round-trip unit tests, +- `HktExample`, +- `docs/HKT.md`. + +### Changed + +- project version updated from `0.4.1` to `0.5.0`, +- `Option` now implements `Kind`, +- `Either` now implements `Kind,R>`, +- `Validated` now implements `Kind,A>`, +- project description, README, ADT documentation, architecture and roadmap updated for the HKT layer. + +### Removed / intentionally omitted + +- no `Kind2` hierarchy is introduced because partial witness types are sufficient for the next milestones, +- no `Functor`, `Applicative`, `Monad`, `Traverse`, effect type, Kleisli or Tagless Final implementation is added yet, +- no reflection, annotation processing, registry or runtime witness objects are introduced, +- no obsolete scripts or generated build outputs are retained. + +### Design notes + +- witness types contain no runtime state and exist only to identify type constructors, +- binary ADTs use partial type application by fixing one parameter in the witness type, +- widening is cast-free because the ADTs implement `Kind` directly, +- unavoidable unchecked narrowing is isolated in exactly three helper methods, +- the milestone intentionally establishes infrastructure rather than higher-order behavior, following SOLID, KISS, DRY and YAGNI. + ## 0.4.1 ### Added diff --git a/README.md b/README.md index 2bf709f..dd0e21d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ An educational implementation of typeclass-oriented functional programming in modern Java. -Milestone **0.4.1** hardens the ADT layer introduced in 0.4.0 with non-empty collections, smart constructors, typed validation errors and domain invariants. +Milestone **0.5.0** introduces a minimal higher-kinded type (HKT) encoding on top of the ADT and algebra layers completed in 0.4.1. ## Goals @@ -15,12 +15,13 @@ The project currently covers: - executable algebraic laws and jqwik property tests, - sealed ADTs: `Option`, `Either` and `Validated`, - compositional `Eq` and `Show` instances, -- `NonEmptyList`, -- smart constructors and invariant-safe domain values, -- typed validation errors, -- domain-level error accumulation with `Validated`. +- `NonEmptyList` and lawful concatenation `Semigroup`, +- smart constructors, typed validation errors and domain invariants, +- a minimal HKT encoding through `Kind`, +- witness types and partial type application, +- centralized widen/narrow helpers. -The project deliberately does **not** yet introduce HKT encoding, `Functor`, `Applicative`, `Monad`, `Traverse`, effects, Kleisli, Tagless Final, Free Monad or optics. +The project deliberately does **not** yet introduce `Functor`, `Applicative`, `Monad`, `Traverse`, effect types, Kleisli, Tagless Final, Free Monad or optics. ## Requirements @@ -47,28 +48,27 @@ src/main/java/io/codeswarm/typeclasses/ │ ├── Eq.java │ ├── Ord.java │ └── Show.java +├── hkt/ +│ ├── Kind.java +│ └── package-info.java ├── data/ │ ├── Option.java +│ ├── OptionK.java +│ ├── OptionKinds.java │ ├── Some.java │ ├── None.java │ ├── Either.java +│ ├── EitherK.java +│ ├── EitherKinds.java │ ├── Left.java │ ├── Right.java │ ├── Validated.java +│ ├── ValidatedK.java +│ ├── ValidatedKinds.java │ ├── Valid.java │ ├── Invalid.java │ └── NonEmptyList.java ├── domain/ -│ ├── ValidationError.java -│ ├── BlankUserId.java -│ ├── BlankPersonName.java -│ ├── PersonNameTooLong.java -│ ├── InvalidAge.java -│ ├── UserId.java -│ ├── PersonName.java -│ ├── Age.java -│ ├── User.java -│ └── UserValidator.java ├── examples/ ├── instances/ └── syntax/ @@ -76,107 +76,91 @@ src/main/java/io/codeswarm/typeclasses/ Reusable law helpers remain in `src/test/java/io/codeswarm/typeclasses/laws/`. -## NonEmptyList +## Higher-kinded type encoding -`NonEmptyList` encodes non-emptiness structurally: +Java can abstract over values of type `A`, but cannot directly abstract over a type constructor like Scala's `F[_]`. Milestone 0.5 introduces: ```java -NonEmptyList values = NonEmptyList.of(1, 2, 3); -``` - -There is no public way to create an empty value. This lets `Invalid` represent failed validation without a separate runtime `isEmpty()` guard. - -```java -public record Invalid(NonEmptyList errors) - implements Validated { +public interface Kind { } ``` -`NonEmptyList` has a natural concatenation `Semigroup` but intentionally no `Monoid`: +The `F` parameter is a witness identifying the constructor. -```text -List -> Monoid> -NonEmptyList -> Semigroup> -``` - -A monoid requires an identity element; an empty `NonEmptyList` cannot exist. +### Option -## Smart constructors +`Option` now implements: -Domain values expose factories that validate raw input and return an ADT instead of throwing for expected validation failures. - -```java -Either id = UserId.from(" user-42 "); -Either age = Age.from(42); +```text +Kind ``` -Invalid input is explicit: +Example: ```java -Age.from(-1); // Left(InvalidAge[-1]) -UserId.from(" "); // Left(BlankUserId[]) +Option option = Option.some(42); +Kind widened = OptionKinds.widen(option); +Option restored = OptionKinds.narrow(widened); ``` -The constructors of `UserId`, `PersonName` and `Age` are private, so invalid states cannot be created through their public APIs. +### Either and partial type application -See [docs/SMART_CONSTRUCTORS.md](docs/SMART_CONSTRUCTORS.md). +`Either` has two type parameters, but a future `Functor` needs a unary constructor. The left type is therefore fixed in a witness: -## Typed validation errors +```text +Either +≈ Kind, Integer> +``` -Expected validation failures are modeled as a closed ADT: +Likewise: ```text -ValidationError -├── BlankUserId -├── BlankPersonName -├── PersonNameTooLong -└── InvalidAge +Validated +≈ Kind, Integer> ``` -This is preferable to using arbitrary strings when callers need to inspect or pattern-match on error categories. +This models partial type application without adding wrapper objects around the ADTs. -## Validated and error accumulation +## Widening and narrowing -`UserValidator` demonstrates accumulation of independent errors: +`widen` requires no cast because each ADT directly implements the relevant `Kind` application. -```java -Validated result = - UserValidator.validate("", "", -10); -``` +`narrow` needs a controlled unchecked cast because Java type erasure cannot prove the witness-to-ADT relationship at runtime. Those casts are intentionally isolated in exactly three helpers: -The result contains three typed errors in a `NonEmptyList`. +- `OptionKinds.narrow`, +- `EitherKinds.narrow`, +- `ValidatedKinds.narrow`. -This implementation is intentionally domain-specific. Generic validation composition is deferred until the project has `Applicative`. +The rest of the codebase should not perform HKT-related unchecked casts. -## ADT helper factories +See [docs/HKT.md](docs/HKT.md). -Milestone 0.4.1 adds small Java-interoperability helpers: +## Existing ADT and domain layer -```java -Option.when(condition, supplier); -Either.cond(condition, rightSupplier, leftSupplier); -Either.fromNullable(value, leftSupplier); -Either.fromOptional(optional, leftSupplier); -``` +Milestone 0.5 preserves the 0.4.1 APIs: -No `flatMap` is added yet; monadic operations will be introduced together with the generic `Monad` milestone. +- `NonEmptyList` models non-empty collections structurally, +- `Invalid` stores `NonEmptyList`, +- `UserId`, `PersonName` and `Age` use smart constructors, +- `ValidationError` is a sealed typed error hierarchy, +- `UserValidator` demonstrates domain-level error accumulation with `Validated`. + +No monadic API is added to those types yet. ## Design principles -- **SRP** — ADTs, domain validation, typeclasses, instances, syntax and laws remain separate. -- **OCP** — new instances and domain validators do not require modifying typeclasses. -- **DIP** — generic functions continue to depend on typeclass abstractions. -- **KISS** — smart constructors use ordinary Java and explicit ADTs. -- **DRY** — law helpers and compositional instances are reused rather than duplicated. -- **YAGNI** — no HKT, generic Applicative validation or effect system is introduced early. +- **SRP** — HKT mechanics live in `hkt` and `*Kinds` helpers rather than inside unrelated typeclasses. +- **OCP** — new type constructors can participate by introducing their own witness and `Kind` implementation. +- **DIP** — future higher-order typeclasses can depend on `Kind` instead of concrete ADTs. +- **KISS** — only one marker abstraction is introduced; there is no `Kind2`, registry or reflection layer. +- **DRY** — unavoidable casts are centralized instead of repeated across future typeclass instances. +- **YAGNI** — `Functor`, `Applicative` and `Monad` remain outside this milestone. ## Examples -`AdtExample` demonstrates `Option`, `Either`, `Validated` and composed `Show` instances. - -`SmartConstructorExample` demonstrates validated domain values. +`HktExample` demonstrates round-trip widening and narrowing for `Option`, `Either` and `Validated`. -`ValidatedUserExample` demonstrates accumulated typed validation errors. +Existing examples continue to demonstrate algebra, ADTs, smart constructors and validation. ## Documentation @@ -186,6 +170,7 @@ No `flatMap` is added yet; monadic operations will be introduced together with t - [Algebraic laws](docs/LAWS.md) - [Algebraic data types](docs/ADT.md) - [Smart constructors](docs/SMART_CONSTRUCTORS.md) +- [Higher-kinded type encoding](docs/HKT.md) - [Implementation tasks](docs/IMPLEMENTATION_TASKS.md) - [Roadmap](docs/ROADMAP.md) - [Changelog](CHANGELOG.md) @@ -193,7 +178,6 @@ No `flatMap` is added yet; monadic operations will be introduced together with t ## Roadmap ```text -0.5 HKT encoding: Kind 0.6 Functor / Applicative / Monad / MonadError 0.7 Foldable / Traverse / Bifunctor 0.8 Id / Eval / IO / FunctionK diff --git a/build.gradle b/build.gradle index bef3db5..15f0da7 100644 --- a/build.gradle +++ b/build.gradle @@ -4,9 +4,9 @@ plugins { } group = 'io.codeswarm' -version = '0.4.1' +version = '0.5.0' -description = 'Educational implementation of typeclass-oriented programming, algebraic data types, smart constructors, domain invariants, compositional instances, and executable algebraic laws in modern Java.' +description = 'Educational implementation of typeclass-oriented programming, algebraic data types, higher-kinded type encoding, compositional instances, smart constructors, and executable algebraic laws in modern Java.' java { toolchain { diff --git a/docs/ADT.md b/docs/ADT.md index 3bd9375..0d2fe92 100644 --- a/docs/ADT.md +++ b/docs/ADT.md @@ -48,3 +48,17 @@ ADT results are also used to create domain values safely. See [SMART_CONSTRUCTOR ## Algebraic connection `NonEmptyList` has a lawful concatenation `Semigroup` but no lawful empty identity. An ordinary `List` can have a concatenation `Monoid` because `List.of()` is an identity element. + +## HKT participation from milestone 0.5 + +The ADTs keep their ordinary Java APIs while also implementing the higher-kinded encoding: + +```text +Option -> Kind +Either -> Kind,R> +Validated -> Kind,A> +``` + +This does not add runtime wrappers and does not alter `map`, `fold`, smart constructors or validation behavior. It only makes the type constructors addressable by generic abstractions that will be introduced in later milestones. + +`Either` and `Validated` demonstrate partial type application: their left/error parameter is fixed in the witness type so the remaining value parameter can occupy the `A` position of `Kind`. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 038e374..2c72e96 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -67,3 +67,53 @@ The accumulation in `UserValidator` is intentionally concrete. A generic solutio Domain examples depend on ADTs. ADTs do not depend on domain classes. Typeclasses do not depend on either domain examples or specific instances. This preserves DIP and keeps the educational abstractions reusable. + +## Milestone 0.5 — HKT layer + +Milestone 0.5 adds a small type-level layer without changing the runtime representation of the ADTs. + +```text + Kind + ▲ + ┌────────────────┼─────────────────┐ + │ │ │ + │ │ │ + Option Either Validated + │ │ │ + │ │ │ + OptionK EitherK ValidatedK +``` + +Conceptually: + +```text +Option ≈ Kind +Either ≈ Kind, A> +Validated ≈ Kind, A> +``` + +### Boundary of unsafe narrowing + +Java erasure means the compiler cannot reconstruct a concrete ADT from `Kind` purely from the witness parameter. The unavoidable casts are therefore centralized in: + +```text +OptionKinds.narrow +EitherKinds.narrow +ValidatedKinds.narrow +``` + +This is an explicit architectural boundary: future higher-order typeclass implementations may call those helpers, but should not introduce additional HKT casts. + +### Dependency direction + +```text +higher-order typeclasses (0.6+) + │ + ▼ + Kind + ▲ + │ + concrete ADTs + witnesses +``` + +`Kind` depends on no ADT. Concrete ADTs opt into the encoding. Future generic abstractions can therefore depend on `Kind` rather than concrete data types. diff --git a/docs/HKT.md b/docs/HKT.md new file mode 100644 index 0000000..04c7e47 --- /dev/null +++ b/docs/HKT.md @@ -0,0 +1,91 @@ +# Higher-Kinded Type Encoding + +## Why this milestone exists + +Java can abstract over ordinary types, but it cannot directly abstract over a type constructor such as `Option` in the same way Scala can abstract over `F[_]`. + +Milestone 0.5 introduces a small encoding: + +```java +public interface Kind { +} +``` + +`F` is a witness identifying a type constructor and `A` is the contained value type. + +## Option + +`Option` now implements: + +```text +Kind +``` + +`OptionK` is a zero-state witness type. It is not instantiated at runtime. + +```java +Option option = Option.some(42); +Kind kind = OptionKinds.widen(option); +Option restored = OptionKinds.narrow(kind); +``` + +## Partial type application + +Binary ADTs need one parameter fixed before they can act as a unary constructor. + +```text +Either + ↓ fix L +EitherK + ↓ apply R +Kind, R> +``` + +Therefore: + +```text +Either +≈ Kind, Integer> +``` + +The same technique is used for `Validated`: + +```text +Validated +≈ Kind, Integer> +``` + +## Widening and narrowing + +`widen` is safe and requires no cast because each ADT directly implements the corresponding `Kind` application. + +`narrow` requires an unchecked cast because Java type erasure cannot prove the relationship between a witness and its concrete ADT. Those casts are intentionally isolated in: + +- `OptionKinds` +- `EitherKinds` +- `ValidatedKinds` + +No other production code should need HKT-related unchecked casts. + +## Limitations + +This is an encoding, not native higher-kinded type support. Compared with Scala or Haskell it has: + +- explicit witness types, +- weaker inference, +- more boilerplate, +- controlled unchecked narrowing. + +The benefit is that later milestones can express abstractions such as: + +```java +interface Functor { + Kind map(Kind value, Function mapper); +} +``` + +without hard-coding `Option`, `Either`, or `Validated`. + +## Deliberate scope boundary + +Milestone 0.5 does **not** implement `Functor`, `Applicative`, `Monad`, `Traverse`, Kleisli, effect types, or Tagless Final. It establishes only the type-level infrastructure required by those abstractions. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 86ba288..1853c80 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,121 +1,104 @@ # Roadmap -## 0.1 — Initial proof of concept +## Completed -Early exploration of typeclass-like patterns in Java. +### 0.1 / 0.1.1 — Foundation -## 0.1.1 — Foundation hardening +Initial proof of concept followed by repository, build, documentation, testing, and typeclass-foundation hardening. -Java 21 baseline, explicit `Show`, dictionary passing, separated instances and documentation cleanup. +### 0.2 — Basic algebraic typeclasses -## 0.2 — Basic algebraic typeclasses +`Eq`, `Ord`, `Semigroup`, `Monoid`, reusable syntax functions, and standard instances. -`Eq`, `Ord`, `Semigroup`, `Monoid`, standard instances and syntax helpers. +### 0.3 — Laws and property-based testing -## 0.3 — Typeclass laws +Reusable algebraic law checks and jqwik property suites. -Executable laws, jqwik property testing and lawful/unlawful examples. +### 0.4 — Algebraic data types -## 0.4 — Algebraic data types +`Option`, `Either`, `Validated`, sealed ADTs, compositional `Eq` and `Show` instances. -`Option`, `Either`, `Validated`, composed `Eq` / `Show` instances and ADT property tests. +### 0.4.1 — ADT hardening -## 0.4.1 — ADT hardening and smart constructors +`NonEmptyList`, smart constructors, typed validation errors, domain invariants, and lawful `Semigroup>`. -**Current milestone.** +### 0.5 — Higher-Kinded Type Encoding -- `NonEmptyList`, -- `Invalid` backed by `NonEmptyList`, -- `Semigroup>`, -- smart constructors, -- typed validation errors, -- invariant-safe domain values, -- Java interoperability helpers for `Option` / `Either`, -- domain-level accumulated validation. +`Kind`, witness types, partial type application, and centralized widen/narrow helpers. -## 0.5 — Higher-kinded type encoding +## Planned -- `Kind`, -- witness types (`OptionK`, `EitherK`, `ValidatedK`), -- isolated narrowing/widening helpers, -- adapters from existing concrete ADTs, -- documentation of Java type-erasure limitations. +### 0.6 — Core higher-order typeclasses -## 0.6 — Functor / Applicative / Monad +- `Functor` +- `Applicative` +- `Monad` +- `MonadError` where the encoding remains practical +- laws for the new typeclasses +- instances for `OptionK`, `EitherK`, and `ValidatedK` as appropriate -- `Functor`, -- `Applicative`, -- `Monad`, -- `MonadError` where justified, -- instances for `Option` and right-biased `Either`, -- Applicative validation accumulation for `Validated`, -- Functor / Applicative / Monad laws. +### 0.7 — Structural typeclasses -## 0.7 — Foldable / Traverse / Bifunctor +- `Foldable` +- `Traverse` +- `Bifunctor` +- `sequence` / `traverse` +- corresponding laws and examples -- `Foldable`, -- `Traverse`, -- `Bifunctor`, -- `sequence` / `traverse`, -- examples such as `List> -> Option>`, -- corresponding laws. +### 0.8 — Effect types and natural transformations -## 0.8 — Effects and natural transformations +- `Id` +- `Eval` +- minimal educational `IO` +- `FunctionK` +- natural-transformation examples -- `Id`, -- `Eval`, -- educational `IO`, -- `FunctionK`, -- effect interpreters, -- stack-safety discussion. +### 0.9 — Kleisli and effectful composition -## 0.9 — Kleisli and effectful composition +- `Kleisli` +- composition of effectful functions +- Reader-style examples -- `Kleisli`, -- composition of effectful functions, -- Reader-style dependency passing where useful, -- workflow examples. +### 0.10 — Tagless Final -## 0.10 — Tagless Final +- algebras parameterized by effect witness `F` +- programs written against abstract capabilities +- multiple interpreters +- tests using pure interpreters -- algebras/capabilities parameterized by `F`, -- programs expressed only in terms of algebras and typeclasses, -- interpreters for `Id`, `Either` and `IO`, -- natural transformations between interpreters, -- comparison with Scala/Cats Tagless Final. +### 0.11 — Free Monad -## 0.11 — Free Monad +- `Free` +- algebra ASTs +- interpreters via natural transformations +- comparison with Tagless Final -- `Free`, -- algebra ASTs, -- interpreters via natural transformations, -- explicit comparison: Tagless Final vs Free Monad. +### 0.12 — Optics -## 0.12 — Optics +- `Lens` +- `Prism` +- `Optional` +- immutable nested updates and composition -- `Lens`, -- `Prism`, -- optional focus / traversal concepts, -- composition for immutable nested updates. +### 0.13 — Advanced composition -## 0.13 — Advanced typeclasses +- `SemigroupK` +- `MonoidK` +- `Alternative` +- richer error accumulation -Candidates include `SemigroupK`, `MonoidK`, `Alternative`, richer error typeclasses and advanced validation composition. +### 0.14 — Derivation and ergonomics -## 0.14 — Derivation and resolution experiments +- derived instances where useful +- optional instance resolution experiments +- summon-like ergonomics without hiding the underlying model -- generated `Eq` / `Show` for records, -- optional annotation processing, -- optional explicit registry / `summon` experiment, -- API naming and package stabilization. +### 0.15 — Performance and API hardening -## 0.15 — Performance and ergonomics +- JMH where measurements are meaningful +- allocation analysis +- API cleanup and compatibility review -- JMH where meaningful, -- allocation analysis, -- API simplification, -- compatibility review. +### 1.0 — Stable educational release -## 1.0 — Stable educational release - -Stable API, complete docs, Java-vs-Scala/Cats comparisons, examples from first-order typeclasses through Tagless Final / Free / optics, and law-checked standard instances. +Stable public API, complete documentation, examples, laws, and a coherent Java-to-functional-programming learning path. diff --git a/src/main/java/io/codeswarm/typeclasses/data/Either.java b/src/main/java/io/codeswarm/typeclasses/data/Either.java index 233c4a1..83f16f6 100644 --- a/src/main/java/io/codeswarm/typeclasses/data/Either.java +++ b/src/main/java/io/codeswarm/typeclasses/data/Either.java @@ -1,5 +1,7 @@ package io.codeswarm.typeclasses.data; +import io.codeswarm.typeclasses.hkt.Kind; + import java.util.Objects; import java.util.Optional; import java.util.function.Function; @@ -15,7 +17,7 @@ * @param left value type * @param right value type */ -public sealed interface Either permits Left, Right { +public sealed interface Either extends Kind, R> permits Left, Right { /** * Creates a left value. diff --git a/src/main/java/io/codeswarm/typeclasses/data/EitherK.java b/src/main/java/io/codeswarm/typeclasses/data/EitherK.java new file mode 100644 index 0000000..b7e446a --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/data/EitherK.java @@ -0,0 +1,15 @@ +package io.codeswarm.typeclasses.data; + +/** + * Witness type identifying {@code Either} after fixing the left type. + * + *

For example, {@code Either} is represented as + * {@code Kind, Integer>}.

+ * + * @param fixed left type + */ +public final class EitherK { + + private EitherK() { + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/data/EitherKinds.java b/src/main/java/io/codeswarm/typeclasses/data/EitherKinds.java new file mode 100644 index 0000000..a4fe5cf --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/data/EitherKinds.java @@ -0,0 +1,40 @@ +package io.codeswarm.typeclasses.data; + +import io.codeswarm.typeclasses.hkt.Kind; +import java.util.Objects; + +/** + * Conversion helpers between {@link Either} and its higher-kinded + * representation with the left type fixed. + */ +public final class EitherKinds { + + private EitherKinds() { + } + + /** + * Widens an {@link Either} to {@code Kind, R>}. + * + * @param either either value to widen + * @param left type + * @param right type + * @return the same value viewed through the HKT encoding + */ + public static Kind, R> widen(final Either either) { + return Objects.requireNonNull(either, "either"); + } + + /** + * Narrows {@code Kind, R>} back to {@link Either}. + * + * @param value higher-kinded either value + * @param left type + * @param right type + * @return corresponding either + */ + @SuppressWarnings("unchecked") + public static Either narrow(final Kind, R> value) { + Objects.requireNonNull(value, "value"); + return (Either) value; + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/data/Option.java b/src/main/java/io/codeswarm/typeclasses/data/Option.java index 0040a6f..35e68b6 100644 --- a/src/main/java/io/codeswarm/typeclasses/data/Option.java +++ b/src/main/java/io/codeswarm/typeclasses/data/Option.java @@ -1,5 +1,7 @@ package io.codeswarm.typeclasses.data; +import io.codeswarm.typeclasses.hkt.Kind; + import java.util.Objects; import java.util.function.Function; import java.util.function.Supplier; @@ -12,7 +14,7 @@ * * @param
contained value type */ -public sealed interface Option permits Some, None { +public sealed interface Option extends Kind permits Some, None { /** * Creates an option containing a non-null value. diff --git a/src/main/java/io/codeswarm/typeclasses/data/OptionK.java b/src/main/java/io/codeswarm/typeclasses/data/OptionK.java new file mode 100644 index 0000000..db8a2bf --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/data/OptionK.java @@ -0,0 +1,15 @@ +package io.codeswarm.typeclasses.data; + +/** + * Witness type identifying the {@link Option} type constructor in the + * higher-kinded encoding. + * + *

The class has no instances and carries no runtime state. It exists only + * at the type level so that {@code Option} can be represented as + * {@code Kind}.

+ */ +public final class OptionK { + + private OptionK() { + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/data/OptionKinds.java b/src/main/java/io/codeswarm/typeclasses/data/OptionKinds.java new file mode 100644 index 0000000..5e42a16 --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/data/OptionKinds.java @@ -0,0 +1,48 @@ +package io.codeswarm.typeclasses.data; + +import io.codeswarm.typeclasses.hkt.Kind; +import java.util.Objects; + +/** + * Conversion helpers between {@link Option} and its higher-kinded + * representation. + * + *

Unchecked narrowing is intentionally isolated in this class so the rest + * of the codebase does not need to repeat casts required by Java type + * erasure.

+ */ +public final class OptionKinds { + + private OptionKinds() { + } + + /** + * Widens an {@link Option} to its generic {@link Kind} representation. + * + * @param option option to widen + * @param
contained value type + * @return the same value viewed as {@code Kind} + */ + public static Kind widen(final Option option) { + return Objects.requireNonNull(option, "option"); + } + + /** + * Narrows an {@link Kind} carrying the {@link OptionK} witness back to an + * {@link Option}. + * + *

The cast is safe for values created through this library because the + * sealed {@code Option} hierarchy is the intended implementation of the + * {@code OptionK} witness. Java cannot prove that relationship after type + * erasure, therefore the cast is centralized here.

+ * + * @param value higher-kinded option value + * @param
contained value type + * @return the corresponding option + */ + @SuppressWarnings("unchecked") + public static Option narrow(final Kind value) { + Objects.requireNonNull(value, "value"); + return (Option) value; + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/data/Validated.java b/src/main/java/io/codeswarm/typeclasses/data/Validated.java index bdae70a..78437bb 100644 --- a/src/main/java/io/codeswarm/typeclasses/data/Validated.java +++ b/src/main/java/io/codeswarm/typeclasses/data/Validated.java @@ -1,5 +1,7 @@ package io.codeswarm.typeclasses.data; +import io.codeswarm.typeclasses.hkt.Kind; + import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -16,7 +18,7 @@ * @param validation error type * @param successful value type */ -public sealed interface Validated permits Valid, Invalid { +public sealed interface Validated extends Kind, A> permits Valid, Invalid { /** * Creates a successful validation result. diff --git a/src/main/java/io/codeswarm/typeclasses/data/ValidatedK.java b/src/main/java/io/codeswarm/typeclasses/data/ValidatedK.java new file mode 100644 index 0000000..45e7ff9 --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/data/ValidatedK.java @@ -0,0 +1,13 @@ +package io.codeswarm.typeclasses.data; + +/** + * Witness type identifying {@code Validated} after fixing the error + * type. + * + * @param fixed validation error type + */ +public final class ValidatedK { + + private ValidatedK() { + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/data/ValidatedKinds.java b/src/main/java/io/codeswarm/typeclasses/data/ValidatedKinds.java new file mode 100644 index 0000000..4c646d4 --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/data/ValidatedKinds.java @@ -0,0 +1,40 @@ +package io.codeswarm.typeclasses.data; + +import io.codeswarm.typeclasses.hkt.Kind; +import java.util.Objects; + +/** + * Conversion helpers between {@link Validated} and its higher-kinded + * representation with the error type fixed. + */ +public final class ValidatedKinds { + + private ValidatedKinds() { + } + + /** + * Widens a {@link Validated} value to {@code Kind, A>}. + * + * @param validated validation value to widen + * @param error type + * @param successful value type + * @return the same value viewed through the HKT encoding + */ + public static Kind, A> widen(final Validated validated) { + return Objects.requireNonNull(validated, "validated"); + } + + /** + * Narrows {@code Kind, A>} back to {@link Validated}. + * + * @param value higher-kinded validation value + * @param error type + * @param successful value type + * @return corresponding validation value + */ + @SuppressWarnings("unchecked") + public static Validated narrow(final Kind, A> value) { + Objects.requireNonNull(value, "value"); + return (Validated) value; + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/examples/AdtExample.java b/src/main/java/io/codeswarm/typeclasses/examples/AdtExample.java index f4836ee..1196eee 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/AdtExample.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/AdtExample.java @@ -22,8 +22,9 @@ private AdtExample() { /** * Runs a small demonstration of Option, Either and Validated. * + * @param args ignored command-line arguments */ - static void main() { + public static void main(final String[] args) { final Option option = Option.some(21).map(value -> value * 2); final Either either = Either.right(21).map(value -> value * 2); final Validated validated = Validated.invalid("name is empty", "age is negative"); diff --git a/src/main/java/io/codeswarm/typeclasses/examples/EqExample.java b/src/main/java/io/codeswarm/typeclasses/examples/EqExample.java index a54445e..1491b8b 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/EqExample.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/EqExample.java @@ -12,8 +12,9 @@ private EqExample() { /** * Runs the equality example. * + * @param args command-line arguments; ignored */ - static void main() { + public static void main(final String[] args) { final var alice30 = new Person("Alice", 30); final var alice40 = new Person("ALICE", 40); System.out.println(EqFunctions.equal(alice30, alice40, PersonInstances.EQ_ALL_FIELDS)); diff --git a/src/main/java/io/codeswarm/typeclasses/examples/HktExample.java b/src/main/java/io/codeswarm/typeclasses/examples/HktExample.java new file mode 100644 index 0000000..827b678 --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/examples/HktExample.java @@ -0,0 +1,41 @@ +package io.codeswarm.typeclasses.examples; + +import io.codeswarm.typeclasses.data.Either; +import io.codeswarm.typeclasses.data.EitherK; +import io.codeswarm.typeclasses.data.EitherKinds; +import io.codeswarm.typeclasses.data.Option; +import io.codeswarm.typeclasses.data.OptionK; +import io.codeswarm.typeclasses.data.OptionKinds; +import io.codeswarm.typeclasses.data.Validated; +import io.codeswarm.typeclasses.data.ValidatedK; +import io.codeswarm.typeclasses.data.ValidatedKinds; +import io.codeswarm.typeclasses.hkt.Kind; + +/** + * Demonstrates widening concrete ADTs to the higher-kinded encoding and + * narrowing them back without changing their runtime values. + */ +public final class HktExample { + + private HktExample() { + } + + /** + * Runs the higher-kinded encoding example. + * + * @param args ignored command-line arguments + */ + public static void main(final String[] args) { + final Option option = Option.some(42); + final Kind optionKind = OptionKinds.widen(option); + System.out.println(OptionKinds.narrow(optionKind)); + + final Either either = Either.right(42); + final Kind, Integer> eitherKind = EitherKinds.widen(either); + System.out.println(EitherKinds.narrow(eitherKind)); + + final Validated validated = Validated.valid(42); + final Kind, Integer> validatedKind = ValidatedKinds.widen(validated); + System.out.println(ValidatedKinds.narrow(validatedKind)); + } +} diff --git a/src/main/java/io/codeswarm/typeclasses/examples/MonoidExample.java b/src/main/java/io/codeswarm/typeclasses/examples/MonoidExample.java index 1ca724f..3799d5c 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/MonoidExample.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/MonoidExample.java @@ -15,8 +15,9 @@ private MonoidExample() { /** * Runs the monoid example. * + * @param args command-line arguments; ignored */ - static void main() { + public static void main(final String[] args) { 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 index d4605e7..660af7c 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/OrdExample.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/OrdExample.java @@ -12,8 +12,9 @@ private OrdExample() { /** * Runs the ordering example. * + * @param args command-line arguments; ignored */ - static void main() { + public static void main(final String[] args) { final var alice = new Person("Alice", 35); final var bob = new Person("Bob", 27); System.out.println(OrdFunctions.min(alice, bob, PersonInstances.ORD_AGE)); diff --git a/src/main/java/io/codeswarm/typeclasses/examples/SemigroupExample.java b/src/main/java/io/codeswarm/typeclasses/examples/SemigroupExample.java index 31594e4..bfba518 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/SemigroupExample.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/SemigroupExample.java @@ -15,8 +15,9 @@ private SemigroupExample() { /** * Runs the semigroup example. * + * @param args command-line arguments; ignored */ - static void main() { + public static void main(final String[] args) { 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 1ae0e23..ed32e20 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/ShowExample.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/ShowExample.java @@ -16,8 +16,9 @@ private ShowExample() { /** * Runs the show example. * + * @param args command-line arguments; ignored */ - static void main() { + public static void main(final String[] args) { final var person = new Person("Alice", 30); System.out.println(ShowFunctions.show(person, PersonInstances.SHOW_COMPACT)); System.out.println(ShowFunctions.show(person, PersonInstances.SHOW_VERBOSE)); diff --git a/src/main/java/io/codeswarm/typeclasses/examples/SmartConstructorExample.java b/src/main/java/io/codeswarm/typeclasses/examples/SmartConstructorExample.java index e14f1ad..9299f2d 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/SmartConstructorExample.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/SmartConstructorExample.java @@ -12,7 +12,7 @@ private SmartConstructorExample() { } /** Runs the smart-constructor example. */ - static void main() { + public static void main(final String[] args) { System.out.println(UserId.from("user-42")); System.out.println(UserId.from(" ")); System.out.println(PersonName.from("Alice")); diff --git a/src/main/java/io/codeswarm/typeclasses/examples/ValidatedUserExample.java b/src/main/java/io/codeswarm/typeclasses/examples/ValidatedUserExample.java index 73d4c58..27b7cad 100644 --- a/src/main/java/io/codeswarm/typeclasses/examples/ValidatedUserExample.java +++ b/src/main/java/io/codeswarm/typeclasses/examples/ValidatedUserExample.java @@ -10,7 +10,7 @@ private ValidatedUserExample() { } /** Runs valid and invalid user-validation examples. */ - static void main() { + public static void main(final String[] args) { System.out.println(UserValidator.validate("user-1", "Alice", 42)); System.out.println(UserValidator.validate("", "", -10)); } diff --git a/src/main/java/io/codeswarm/typeclasses/hkt/Kind.java b/src/main/java/io/codeswarm/typeclasses/hkt/Kind.java new file mode 100644 index 0000000..3945f95 --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/hkt/Kind.java @@ -0,0 +1,19 @@ +package io.codeswarm.typeclasses.hkt; + +/** + * Encodes an application of a higher-kinded type constructor {@code F} to a + * value type {@code A}. + * + *

Java cannot abstract directly over type constructors such as + * {@code Option}. This marker interface provides a lightweight encoding + * that allows later typeclasses to describe operations over a constructor + * independently of the contained value type.

+ * + *

The {@code F} parameter is a witness type. For example, + * {@code Option} implements {@code Kind}.

+ * + * @param witness type identifying the type constructor + * @param contained value type + */ +public interface Kind { +} diff --git a/src/main/java/io/codeswarm/typeclasses/hkt/package-info.java b/src/main/java/io/codeswarm/typeclasses/hkt/package-info.java new file mode 100644 index 0000000..f1fd984 --- /dev/null +++ b/src/main/java/io/codeswarm/typeclasses/hkt/package-info.java @@ -0,0 +1,9 @@ +/** + * Higher-kinded type encoding used to represent type-constructor applications + * in Java. + * + *

The package deliberately contains only the minimal {@code Kind} + * abstraction. Higher-order typeclasses such as Functor, Applicative and Monad + * are introduced in later milestones.

+ */ +package io.codeswarm.typeclasses.hkt; diff --git a/src/main/java/io/codeswarm/typeclasses/instances/LocalDateInstances.java b/src/main/java/io/codeswarm/typeclasses/instances/LocalDateInstances.java index 583c01d..f48a7ea 100644 --- a/src/main/java/io/codeswarm/typeclasses/instances/LocalDateInstances.java +++ b/src/main/java/io/codeswarm/typeclasses/instances/LocalDateInstances.java @@ -17,7 +17,7 @@ public final class LocalDateInstances { /** Renders dates in ISO-8601 local-date format. */ public static final Show ISO_SHOW = - DateTimeFormatter.ISO_LOCAL_DATE::format; + date -> DateTimeFormatter.ISO_LOCAL_DATE.format(date); /** Compares dates using standard value equality. */ public static final Eq EQ = LocalDate::equals; diff --git a/src/main/java/io/codeswarm/typeclasses/instances/OptionInstances.java b/src/main/java/io/codeswarm/typeclasses/instances/OptionInstances.java index 2a39e17..2fce08b 100644 --- a/src/main/java/io/codeswarm/typeclasses/instances/OptionInstances.java +++ b/src/main/java/io/codeswarm/typeclasses/instances/OptionInstances.java @@ -4,6 +4,7 @@ import io.codeswarm.typeclasses.core.Show; import io.codeswarm.typeclasses.data.None; import io.codeswarm.typeclasses.data.Option; +import io.codeswarm.typeclasses.data.Some; import java.util.Objects; /** diff --git a/src/test/java/io/codeswarm/typeclasses/domain/UserValidatorTest.java b/src/test/java/io/codeswarm/typeclasses/domain/UserValidatorTest.java index e690374..436bdb4 100644 --- a/src/test/java/io/codeswarm/typeclasses/domain/UserValidatorTest.java +++ b/src/test/java/io/codeswarm/typeclasses/domain/UserValidatorTest.java @@ -24,6 +24,11 @@ void shouldCreateValidUser() { void shouldAccumulateAllErrors() { final var result = UserValidator.validate("", "", -1); assertTrue(result.isInvalid()); - assertEquals(3, Optional.ofNullable(result.fold(NonEmptyList::size, ignored -> 0)).get()); + final int errorCount = + result.fold( + NonEmptyList::size, + ignored -> 0); + + assertEquals(3, errorCount); } } diff --git a/src/test/java/io/codeswarm/typeclasses/hkt/EitherKindsTest.java b/src/test/java/io/codeswarm/typeclasses/hkt/EitherKindsTest.java new file mode 100644 index 0000000..80caeff --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/hkt/EitherKindsTest.java @@ -0,0 +1,30 @@ +package io.codeswarm.typeclasses.hkt; + +import io.codeswarm.typeclasses.data.Either; +import io.codeswarm.typeclasses.data.EitherK; +import io.codeswarm.typeclasses.data.EitherKinds; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * Verifies higher-kinded widening and narrowing for {@link Either}. + */ +class EitherKindsTest { + + /** + * Ensures partial application keeps the left type fixed while the right + * type occupies the value position of {@link Kind}. + */ + @Test + void shouldRoundTripEither() { + final Either original = Either.right(42); + final Kind, Integer> widened = EitherKinds.widen(original); + final Either narrowed = EitherKinds.narrow(widened); + + assertSame(original, widened); + assertSame(original, narrowed); + assertEquals(original, narrowed); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/hkt/OptionKindsTest.java b/src/test/java/io/codeswarm/typeclasses/hkt/OptionKindsTest.java new file mode 100644 index 0000000..579abaa --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/hkt/OptionKindsTest.java @@ -0,0 +1,29 @@ +package io.codeswarm.typeclasses.hkt; + +import io.codeswarm.typeclasses.data.Option; +import io.codeswarm.typeclasses.data.OptionK; +import io.codeswarm.typeclasses.data.OptionKinds; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * Verifies higher-kinded widening and narrowing for {@link Option}. + */ +class OptionKindsTest { + + /** + * Ensures a round-trip preserves the concrete option value and identity. + */ + @Test + void shouldRoundTripOption() { + final Option original = Option.some(42); + final Kind widened = OptionKinds.widen(original); + final Option narrowed = OptionKinds.narrow(widened); + + assertSame(original, widened); + assertSame(original, narrowed); + assertEquals(original, narrowed); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/hkt/ValidatedKindsTest.java b/src/test/java/io/codeswarm/typeclasses/hkt/ValidatedKindsTest.java new file mode 100644 index 0000000..8e91b0d --- /dev/null +++ b/src/test/java/io/codeswarm/typeclasses/hkt/ValidatedKindsTest.java @@ -0,0 +1,30 @@ +package io.codeswarm.typeclasses.hkt; + +import io.codeswarm.typeclasses.data.Validated; +import io.codeswarm.typeclasses.data.ValidatedK; +import io.codeswarm.typeclasses.data.ValidatedKinds; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * Verifies higher-kinded widening and narrowing for {@link Validated}. + */ +class ValidatedKindsTest { + + /** + * Ensures widening and narrowing do not change the concrete validation + * value. + */ + @Test + void shouldRoundTripValidated() { + final Validated original = Validated.valid(42); + final Kind, Integer> widened = ValidatedKinds.widen(original); + final Validated narrowed = ValidatedKinds.narrow(widened); + + assertSame(original, widened); + assertSame(original, narrowed); + assertEquals(original, narrowed); + } +} diff --git a/src/test/java/io/codeswarm/typeclasses/properties/AdtInstancesProperties.java b/src/test/java/io/codeswarm/typeclasses/properties/AdtInstancesProperties.java index c6beff9..5a98094 100644 --- a/src/test/java/io/codeswarm/typeclasses/properties/AdtInstancesProperties.java +++ b/src/test/java/io/codeswarm/typeclasses/properties/AdtInstancesProperties.java @@ -9,6 +9,7 @@ import io.codeswarm.typeclasses.instances.StringInstances; import io.codeswarm.typeclasses.instances.ValidatedInstances; import io.codeswarm.typeclasses.laws.EqLaws; +import java.util.List; import net.jqwik.api.ForAll; import net.jqwik.api.Property; diff --git a/src/test/java/io/codeswarm/typeclasses/syntax/MonoidFunctionsTest.java b/src/test/java/io/codeswarm/typeclasses/syntax/MonoidFunctionsTest.java index 67bea7b..7c30369 100644 --- a/src/test/java/io/codeswarm/typeclasses/syntax/MonoidFunctionsTest.java +++ b/src/test/java/io/codeswarm/typeclasses/syntax/MonoidFunctionsTest.java @@ -21,7 +21,7 @@ void shouldUseSelectedMonoid() { /** 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)); + 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/SemigroupFunctionsTest.java b/src/test/java/io/codeswarm/typeclasses/syntax/SemigroupFunctionsTest.java index f2ae092..e37fc0e 100644 --- a/src/test/java/io/codeswarm/typeclasses/syntax/SemigroupFunctionsTest.java +++ b/src/test/java/io/codeswarm/typeclasses/syntax/SemigroupFunctionsTest.java @@ -23,6 +23,6 @@ void shouldCombineNonEmptyIterable() { @Test void shouldRejectEmptyIterable() { assertThrows(IllegalArgumentException.class, - () -> SemigroupFunctions.combineAll(List.of(), MAXIMUM)); + () -> SemigroupFunctions.combineAll(List.of(), MAXIMUM)); } }