Extending Higher Kinded Type Simulation
You can add support for new Java types (type constructors) to the Higher-Kinded-J simulation framework, allowing them to be used with type classes like Functor
, Monad
, etc.
There are two main scenarios:
- Adapting External Types: For types you don't own (e.g., JDK classes like
java.util.Set
,java.util.Map
, or classes from other libraries). - Integrating Custom Library Types: For types defined within your own project or a library you control, where you can modify the type itself.
The core pattern involves creating:
- An
XxxKind
interface with a nestedWitness
type (this remains the same). - An
XxxConverterOps
interface defining thewiden
andnarrow
operations for the specific type. - An
XxxKindHelper
enum that implementsXxxConverterOps
and provides a singleton instance (e.g.,SET
,MY_TYPE
) for accessing these operations as instance methods. - Type class instances (e.g., for
Functor
,Monad
).
For external types, an additional XxxHolder
record is typically used internally by the helper enum to wrap the external type.
Scenario 1: Adapting an External Type (e.g., java.util.Set<A>
)
Since we cannot modify java.util.Set
to directly implement our Kind
structure, we need a wrapper (a Holder
).
Goal: Simulate java.util.Set<A>
as Kind<SetKind.Witness, A>
and provide Functor
, Applicative
, and Monad
instances for it.
-
Create the
Kind
Interface with Witness (SetKind.java
):- Define a marker interface that extends
Kind<SetKind.Witness, A>
. - Inside this interface, define a
static final class Witness {}
which will serve as the phantom typeF
forSet
.
package org.higherkindedj.hkt.set; // Example package import org.higherkindedj.hkt.Kind; import org.jspecify.annotations.NullMarked; /** * Kind interface marker for java.util.Set<A>. * The Witness type F = SetKind.Witness * The Value type A = A */ @NullMarked public interface SetKind<A> extends Kind<SetKind.Witness, A> { /** * Witness type for {@link java.util.Set} to be used with {@link Kind}. */ final class Witness { private Witness() {} } }
- Define a marker interface that extends
Create the ConverterOps
Interface (SetConverterOps.java
):
* Define an interface specifying the widen
and narrow
methods for Set
.
```java
package org.higherkindedj.hkt.set;
import java.util.Set;
import org.higherkindedj.hkt.Kind;
import org.higherkindedj.hkt.exception.KindUnwrapException; // If narrow throws it
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
public interface SetConverterOps {
<A> @NonNull Kind<SetKind.Witness, A> widen(@NonNull Set<A> set);
<A> @NonNull Set<A> narrow(@Nullable Kind<SetKind.Witness, A> kind) throws KindUnwrapException;
}
```
-
Create the
KindHelper
Enum with an InternalHolder
(SetKindHelper.java
):- Define an
enum
(e.g.,SetKindHelper
) that implementsSetConverterOps
. - Provide a singleton instance (e.g.,
SET
). - Inside this helper, define a package-private
record SetHolder<A>(@NonNull Set<A> set) implements SetKind<A> {}
. This record wraps the actualjava.util.Set
. widen
method: Takes the Java type (e.g.,Set<A>
), performs null checks, and returns a newSetHolder<>(set)
cast toKind<SetKind.Witness, A>
.narrow
method: TakesKind<SetKind.Witness, A> kind
, performs null checks, verifieskind instanceof SetHolder
, extracts the underlyingSet<A>
, and returns it. It throwsKindUnwrapException
for any structural invalidity.
package org.higherkindedj.hkt.set; import java.util.Objects; import java.util.Set; import org.higherkindedj.hkt.Kind; import org.higherkindedj.hkt.exception.KindUnwrapException; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; public enum SetKindHelper implements SetConverterOps { SET; // Singleton instance // Error messages can be static final within the enum private static final String ERR_INVALID_KIND_NULL = "Cannot narrow null Kind for Set"; private static final String ERR_INVALID_KIND_TYPE = "Kind instance is not a SetHolder: "; private static final String ERR_INVALID_KIND_TYPE_NULL = "Input Set cannot be null for widen"; // Holder Record (package-private for testability if needed) record SetHolder<AVal>(@NonNull Set<AVal> set) implements SetKind<AVal> { } @Override public <A> @NonNull Kind<SetKind.Witness, A> widen(@NonNull Set<A> set) { Objects.requireNonNull(set, ERR_INVALID_KIND_TYPE_NULL); return new SetHolder<>(set); } @Override public <A> @NonNull Set<A> narrow(@Nullable Kind<SetKind.Witness, A> kind) { if (kind == null) { throw new KindUnwrapException(ERR_INVALID_KIND_NULL); } if (kind instanceof SetHolder<?> holder) { // SetHolder's 'set' component is @NonNull, so holder.set() is guaranteed non-null. return (Set<A>) holder.set(); } else { throw new KindUnwrapException(ERR_INVALID_KIND_TYPE + kind.getClass().getName()); } } }
- Define an
Scenario 2: Integrating a Custom Library Type
If you are defining a new type within your library (e.g., a custom MyType<A>
), you can design it to directly participate in the HKT simulation. This approach typically doesn't require an explicit Holder
record if your type can directly implement the XxxKind
interface.
-
Define Your Type and its
Kind
Interface:- Your custom type (e.g.,
MyType<A>
) directly implements its correspondingMyTypeKind<A>
interface. MyTypeKind<A>
extendsKind<MyType.Witness, A>
and defines the nestedWitness
class. (This part remains unchanged).
package org.example.mytype; import org.higherkindedj.hkt.Kind; import org.jspecify.annotations.NullMarked; // 1. The Kind Interface with Witness @NullMarked public interface MyTypeKind<A> extends Kind<MyType.Witness, A> { /** Witness type for MyType. */ final class Witness { private Witness() {} } } // 2. Your Custom Type directly implements its Kind interface public record MyType<A>(A value) implements MyTypeKind<A> { // ... constructors, methods for MyType ... }
- Your custom type (e.g.,
-
Create the
ConverterOps
Interface (MyTypeConverterOps.java
):- Define an interface specifying the
widen
andnarrow
methods forMyType
.
package org.example.mytype; import org.higherkindedj.hkt.Kind; import org.higherkindedj.hkt.exception.KindUnwrapException; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; public interface MyTypeConverterOps { <A> @NonNull Kind<MyType.Witness, A> widen(@NonNull MyType<A> myTypeValue); <A> @NonNull MyType<A> narrow(@Nullable Kind<MyType.Witness, A> kind) throws KindUnwrapException; }
- Define an interface specifying the
-
Create the
KindHelper
Enum (MyTypeKindHelper.java
):- Define an
enum
(e.g.,MyTypeKindHelper
) that implementsMyTypeConverterOps
. - Provide a singleton instance (e.g.,
MY_TYPE
). widen(MyType<A> myTypeValue)
: SinceMyType<A>
is already aMyTypeKind<A>
(and thus aKind
), this method performs a null check and then a direct cast.narrow(Kind<MyType.Witness, A> kind)
: This method checksif (kind instanceof MyType<?> myTypeInstance)
and then casts and returnsmyTypeInstance
.
package org.example.mytype; import org.higherkindedj.hkt.Kind; import org.higherkindedj.hkt.exception.KindUnwrapException; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import java.util.Objects; public enum MyTypeKindHelper implements MyTypeConverterOps { MY_TYPE; // Singleton instance private static final String ERR_INVALID_KIND_NULL = "Cannot narrow null Kind for MyType"; private static final String ERR_INVALID_KIND_TYPE = "Kind instance is not a MyType: "; @Override @SuppressWarnings("unchecked") // MyType<A> is MyTypeKind<A> is Kind<MyType.Witness, A> public <A> @NonNull Kind<MyType.Witness, A> widen(@NonNull MyType<A> myTypeValue) { Objects.requireNonNull(myTypeValue, "Input MyType cannot be null for widen"); return (MyTypeKind<A>) myTypeValue; // Direct cast } @Override @SuppressWarnings("unchecked") public <A> @NonNull MyType<A> narrow(@Nullable Kind<MyType.Witness, A> kind) { if (kind == null) { throw new KindUnwrapException(ERR_INVALID_KIND_NULL); } // Check if it's an instance of your actual type if (kind instanceof MyType<?> myTypeInstance) { // Pattern match for MyType return (MyType<A>) myTypeInstance; // Direct cast } else { throw new KindUnwrapException(ERR_INVALID_KIND_TYPE + kind.getClass().getName()); } } }
- Define an
-
Implement Type Class Instances:
- These will be similar to the external type scenario (e.g.,
MyTypeMonad implements Monad<MyType.Witness>
), usingMyTypeKindHelper.MY_TYPE.widen(...)
andMyTypeKindHelper.MY_TYPE.narrow(...)
(or with static importMY_TYPE.widen(...)
).
- These will be similar to the external type scenario (e.g.,
- Immutability: Favor immutable data structures for your
Holder
or custom type if possible, as this aligns well with functional programming principles. - Null Handling: Be very clear about null handling. Can the wrapped Java type be null? Can the value
A
inside be null?KindHelper
'swiden
method should typically reject a null container itself.Monad.of(null)
behavior depends on the specific monad (e.g.,OptionalMonad.OPTIONAL_MONAD.of(null)
is empty viaOPTIONAL.widen(Optional.empty())
,ListMonad.LIST_MONAD.of(null)
might be an empty list or a list with a null element based on its definition). - Testing: Thoroughly test your
XxxKindHelper
enum (especiallynarrow
with invalid inputs) and your type class instances (Functor, Applicative, Monad laws).
By following these patterns, you can integrate new or existing types into the Higher-Kinded-J framework, enabling them to be used with generic functional abstractions. The KindHelper
enums, along with their corresponding ConverterOps
interfaces, provide a standardized way to handle the widen
and narrow
conversions.