diff --git a/src/main/java/com/garciat/typeclasses/TypeClasses.java b/src/main/java/com/garciat/typeclasses/TypeClasses.java new file mode 100644 index 0000000..bafda6d --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/TypeClasses.java @@ -0,0 +1,200 @@ +package com.garciat.typeclasses; + +import static com.garciat.typeclasses.api.TypeClass.Witness.Overlap.OVERLAPPABLE; +import static com.garciat.typeclasses.api.TypeClass.Witness.Overlap.OVERLAPPING; +import static java.lang.reflect.AccessFlag.PUBLIC; +import static java.lang.reflect.AccessFlag.STATIC; + +import com.garciat.typeclasses.api.Ctx; +import com.garciat.typeclasses.api.Ty; +import com.garciat.typeclasses.api.TypeClass; +import com.garciat.typeclasses.impl.Lists; +import com.garciat.typeclasses.impl.ZeroOneMore; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class TypeClasses { + public static T witness(Ty ty, Ctx... context) { + return switch (summon(ParsedType.parse(ty.type()), parseContext(context))) { + case Either.Left(SummonError error) -> + throw new WitnessResolutionException(error); + case Either.Right(Object instance) -> { + @SuppressWarnings("unchecked") + T typedInstance = (T) instance; + yield typedInstance; + } + }; + } + + private static List parseContext(Ctx[] context) { + return Arrays.stream(context) + .map(ctx -> new ContextInstance(ctx.instance(), ParsedType.parse(ctx.type()))) + .toList(); + } + + public static class WitnessResolutionException extends RuntimeException { + private WitnessResolutionException(SummonError error) { + super(error.format()); + } + } + + private sealed interface SummonError { + record NotFound(ParsedType target) implements SummonError {} + + record Ambiguous(ParsedType target, List candidates) implements SummonError {} + + record Nested(ParsedType target, SummonError cause) implements SummonError {} + + default String format() { + return switch (this) { + case NotFound(ParsedType target) -> "No witness found for type: " + target.format(); + case Ambiguous(ParsedType target, List candidates) -> + "Ambiguous witnesses found for type: " + + target.format() + + "\nCandidates:\n" + + candidates.stream() + .map(c -> c.rule().toString()) + .collect(Collectors.joining("\n")) + .indent(2); + case Nested(ParsedType target, SummonError cause) -> + "While summoning witness for type: " + + target.format() + + "\nCaused by: " + + cause.format().indent(2); + }; + } + } + + private static Either summon( + ParsedType target, List context) { + return switch (ZeroOneMore.of(findCandidates(target, context))) { + case ZeroOneMore.One(Candidate(var rule, var requirements)) -> + summonAll(requirements, context) + .map(rule::instantiate) + .mapLeft(error -> new SummonError.Nested(target, error)); + case ZeroOneMore.Zero() -> Either.left(new SummonError.NotFound(target)); + case ZeroOneMore.More(var candidates) -> + Either.left(new SummonError.Ambiguous(target, candidates)); + }; + } + + private static Either> summonAll( + List targets, List context) { + return Either.traverse(targets, target -> summon(target, context)); + } + + private record Candidate(WitnessRule rule, List requirements) {} + + private static List findCandidates(ParsedType target, List context) { + return Stream.concat( + context.stream(), reduceOverlapping(findRules(target)).stream()) + .flatMap( + rule -> + rule + .tryMatch(target) + .map(requirements -> new Candidate(rule, requirements)) + .stream()) + .toList(); + } + + /** + * @implSpec 6.8.8.5. + * Overlapping instances + */ + private static List reduceOverlapping(List candidates) { + return candidates.stream() + .filter( + iX -> + candidates.stream().filter(iY -> iX != iY).noneMatch(iY -> isOverlappedBy(iX, iY))) + .toList(); + } + + private static boolean isOverlappedBy(InstanceConstructor iX, InstanceConstructor iY) { + return (iX.overlap() == OVERLAPPABLE || iY.overlap() == OVERLAPPING) + && isSubstitutionInstance(iX, iY) + && !isSubstitutionInstance(iY, iX); + } + + private static boolean isSubstitutionInstance( + InstanceConstructor base, InstanceConstructor reference) { + return Unification.unify(base.func().returnType(), reference.func().returnType()) + .fold(() -> false, map -> !map.isEmpty()); + } + + private static List findRules(ParsedType target) { + return switch (target) { + case ParsedType.App(var fun, var arg) -> Lists.concat(findRules(fun), findRules(arg)); + case ParsedType.Const(var java) -> rulesOf(java); + case ParsedType.Var(var java) -> List.of(); + case ParsedType.ArrayOf(var elem) -> List.of(); + case ParsedType.Primitive(var java) -> List.of(); + }; + } + + private static List rulesOf(Class cls) { + return Arrays.stream(cls.getDeclaredMethods()) + .filter(TypeClasses::isWitnessMethod) + .map(FuncType::parse) + .map(InstanceConstructor::new) + .toList(); + } + + private static boolean isWitnessMethod(Method m) { + return m.accessFlags().contains(PUBLIC) + && m.accessFlags().contains(STATIC) + && m.isAnnotationPresent(TypeClass.Witness.class); + } + + private sealed interface WitnessRule { + Maybe> tryMatch(ParsedType target); + + Object instantiate(List dependencies); + } + + private record ContextInstance(Object instance, ParsedType type) implements WitnessRule { + @Override + public Maybe> tryMatch(ParsedType target) { + return target.equals(type) ? Maybe.just(List.of()) : Maybe.nothing(); + } + + @Override + public Object instantiate(List dependencies) { + return instance; + } + + @Override + public String toString() { + return "context instance: " + type.format(); + } + } + + private record InstanceConstructor(FuncType func) implements WitnessRule { + public TypeClass.Witness.Overlap overlap() { + return func.java().getAnnotation(TypeClass.Witness.class).overlap(); + } + + @Override + public Maybe> tryMatch(ParsedType target) { + return Unification.unify(func.returnType(), target) + .map(map -> Unification.substituteAll(map, func.paramTypes())); + } + + @Override + public Object instantiate(List dependencies) { + try { + return func.java().invoke(null, dependencies.toArray()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + return func.format(); + } + } +} diff --git a/src/main/java/com/garciat/typeclasses/Main.java b/src/main/java/com/garciat/typeclasses/TypeDefinitions.java similarity index 77% rename from src/main/java/com/garciat/typeclasses/Main.java rename to src/main/java/com/garciat/typeclasses/TypeDefinitions.java index 706fc5f..00001ff 100644 --- a/src/main/java/com/garciat/typeclasses/Main.java +++ b/src/main/java/com/garciat/typeclasses/TypeDefinitions.java @@ -1,20 +1,23 @@ +// This file contains all type class and data type definitions +// TODO: Extract to individual files in classes/ and types/ packages package com.garciat.typeclasses; -import static com.garciat.typeclasses.Functions.curry; -import static com.garciat.typeclasses.Functions.flip; import static com.garciat.typeclasses.TyEq.refl; -import static com.garciat.typeclasses.TypeClass.Witness.Overlap.OVERLAPPABLE; -import static com.garciat.typeclasses.TypeClass.Witness.Overlap.OVERLAPPING; -import static com.garciat.typeclasses.TypeClasses.witness; -import static java.lang.reflect.AccessFlag.PUBLIC; -import static java.lang.reflect.AccessFlag.STATIC; -import static java.util.Objects.requireNonNull; +import static com.garciat.typeclasses.api.TypeClass.Witness.Overlap.OVERLAPPING; +import static com.garciat.typeclasses.impl.Functions.curry; +import static com.garciat.typeclasses.impl.Functions.flip; import static java.util.function.Function.identity; -import com.garciat.typeclasses.Kind.KArr; -import com.garciat.typeclasses.Kind.KStar; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +import com.garciat.typeclasses.api.TypeClass; +import com.garciat.typeclasses.api.hkt.Kind; +import com.garciat.typeclasses.api.hkt.Kind.KArr; +import com.garciat.typeclasses.api.hkt.Kind.KStar; +import com.garciat.typeclasses.api.hkt.TApp; +import com.garciat.typeclasses.api.hkt.TPar; +import com.garciat.typeclasses.api.hkt.TagBase; +import com.garciat.typeclasses.classes.Ordering; +import com.garciat.typeclasses.impl.Lists; +import com.garciat.typeclasses.impl.Maps; import java.lang.reflect.GenericArrayType; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -24,7 +27,6 @@ import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -37,89 +39,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -public class Main { - public static void main(String[] args) { - System.out.println(Show.show(witness(new Ty<>() {}), new int[] {1, 2, 3, 4, 5})); - - System.out.println(Show.show(witness(new Ty<>() {}), new Integer[] {1, 2, 3, 4, 5})); - - Map>> m1 = - Map.of( - "a", - List.of(Optional.of(1), Optional.empty()), - "b", - List.of(Optional.of(2), Optional.of(3))); - - System.out.printf("show(m1) = %s\n", Show.show(witness(new Ty<>() {}), m1)); - - List> sums = List.of(new Sum<>(3), new Sum<>(5), new Sum<>(10)); - - System.out.printf( - "combineAll(%s) = %s\n", sums, Monoid.combineAll(witness(new Ty<>() {}), sums)); - - System.out.printf("eq(m1, m1) = %s\n", Eq.eq(witness(new Ty<>() {}), m1, m1)); - - Optional m5 = Optional.of(5); - Optional m10 = Optional.of(10); - - System.out.printf( - "compare(%s, %s) = %s\n", m5, m10, Ord.compare(witness(new Ty<>() {}), m5, m10)); - - Arbitrary, List>>> arbFunc = - witness(new Ty<>() {}); - var f = arbFunc.arbitrary().generate(42L, 10); - - System.out.println("f(10) = " + f.apply(Optional.of(5))); - - System.out.println( - Traversable.traverse( - witness(new Ty<>() {}), witness(new Ty<>() {}), JavaList.of(1, 2, 3), Maybe::just)); - - System.out.println(Show.show(witness(new Ty<>() {}), FwdList.of('h', 'e', 'l', 'l', 'o'))); - - example(witness(new Ty<>() {}), 123); - - F3 sum = SumAllInt.of(witness(new Ty<>() {})); - System.out.println(sum.apply(1, 2, 3)); - - F3, Integer, Void> printer = PrintAll.of(witness(new Ty<>() {})); - printer.apply("Items:", JavaList.of("apple", "banana", "cherry"), 0); - - Foldable foldableFwdList = witness(new Ty<>() {}); - - System.out.println(foldableFwdList.length(FwdList.of(1, 2, 3, 4, 5))); - - System.out.println(foldableFwdList.toList(FwdList.of(1, 2, 3))); - } - - static void example(Show showA, A value) { - System.out.println(Show.show(witness(new Ty<>() {}, new Ctx<>(showA) {}), JavaList.of(value))); - } -} - -// ==== Type System ==== - -// This is how we get basic kind checking in Java -interface Kind { - sealed interface Base {} - - // KStar = * - final class KStar implements Base {} - - // KArr k = * -> k - final class KArr implements Base {} -} - -abstract class TagBase implements Kind {} - -// Full application of a unary type constructor -// TApp :: (* -> *) -> * -> * -interface TApp>, A> extends Kind {} - -// Partial application of a binary type constructor -// TPar :: (* -> * -> *) -> * -> (* -> *) -interface TPar>>, A> extends Kind> {} +// Internal type parsing sealed interface ParsedType { record Var(TypeVariable java) implements ParsedType {} @@ -188,6 +109,7 @@ private static Maybe> parseAppType(ParameterizedType t) { } } +// Internal unification algorithm class Unification { public static Maybe> unify(ParsedType t1, ParsedType t2) { return switch (Pair.of(t1, t2)) { @@ -231,6 +153,7 @@ public static List substituteAll( } } +// Internal function type representation record FuncType(Method java, List paramTypes, ParsedType returnType) { public String format() { return String.format( @@ -256,227 +179,10 @@ public static FuncType parse(Method method) { } // === Type Class System === +// TypeClass, Ty, Ctx are now in com.garciat.typeclasses.api package -@Retention(RetentionPolicy.RUNTIME) -@interface TypeClass { - @Retention(RetentionPolicy.RUNTIME) - @interface Witness { - Overlap overlap() default Overlap.NONE; - - enum Overlap { - NONE, - OVERLAPPING, - OVERLAPPABLE - } - } -} - -interface Ty { - default Type type() { - return requireNonNull( - ((ParameterizedType) getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0]); - } -} - -abstract class Ctx { - private final T instance; - - Ctx(T instance) { - this.instance = instance; - } - - public T instance() { - return instance; - } - - public Type type() { - return requireNonNull( - ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]); - } -} - -class TypeClasses { - public static T witness(Ty ty, Ctx... context) { - return switch (summon(ParsedType.parse(ty.type()), parseContext(context))) { - case Either.Left(SummonError error) -> - throw new WitnessResolutionException(error); - case Either.Right(Object instance) -> { - @SuppressWarnings("unchecked") - T typedInstance = (T) instance; - yield typedInstance; - } - }; - } - - private static List parseContext(Ctx[] context) { - return Arrays.stream(context) - .map(ctx -> new ContextInstance(ctx.instance(), ParsedType.parse(ctx.type()))) - .toList(); - } - - public static class WitnessResolutionException extends RuntimeException { - private WitnessResolutionException(SummonError error) { - super(error.format()); - } - } - - private sealed interface SummonError { - record NotFound(ParsedType target) implements SummonError {} - - record Ambiguous(ParsedType target, List candidates) implements SummonError {} - - record Nested(ParsedType target, SummonError cause) implements SummonError {} - - default String format() { - return switch (this) { - case NotFound(ParsedType target) -> "No witness found for type: " + target.format(); - case Ambiguous(ParsedType target, List candidates) -> - "Ambiguous witnesses found for type: " - + target.format() - + "\nCandidates:\n" - + candidates.stream() - .map(c -> c.rule().toString()) - .collect(Collectors.joining("\n")) - .indent(2); - case Nested(ParsedType target, SummonError cause) -> - "While summoning witness for type: " - + target.format() - + "\nCaused by: " - + cause.format().indent(2); - }; - } - } - - private static Either summon( - ParsedType target, List context) { - return switch (ZeroOneMore.of(findCandidates(target, context))) { - case ZeroOneMore.One(Candidate(var rule, var requirements)) -> - summonAll(requirements, context) - .map(rule::instantiate) - .mapLeft(error -> new SummonError.Nested(target, error)); - case ZeroOneMore.Zero() -> Either.left(new SummonError.NotFound(target)); - case ZeroOneMore.More(var candidates) -> - Either.left(new SummonError.Ambiguous(target, candidates)); - }; - } - - private static Either> summonAll( - List targets, List context) { - return Either.traverse(targets, target -> summon(target, context)); - } - - private record Candidate(WitnessRule rule, List requirements) {} - - private static List findCandidates(ParsedType target, List context) { - return Stream.concat( - context.stream(), reduceOverlapping(findRules(target)).stream()) - .flatMap( - rule -> - rule - .tryMatch(target) - .map(requirements -> new Candidate(rule, requirements)) - .stream()) - .toList(); - } - - /** - * @implSpec 6.8.8.5. - * Overlapping instances - */ - private static List reduceOverlapping(List candidates) { - return candidates.stream() - .filter( - iX -> - candidates.stream().filter(iY -> iX != iY).noneMatch(iY -> isOverlappedBy(iX, iY))) - .toList(); - } - - private static boolean isOverlappedBy(InstanceConstructor iX, InstanceConstructor iY) { - return (iX.overlap() == OVERLAPPABLE || iY.overlap() == OVERLAPPING) - && isSubstitutionInstance(iX, iY) - && !isSubstitutionInstance(iY, iX); - } - - private static boolean isSubstitutionInstance( - InstanceConstructor base, InstanceConstructor reference) { - return Unification.unify(base.func().returnType(), reference.func().returnType()) - .fold(() -> false, map -> !map.isEmpty()); - } - - private static List findRules(ParsedType target) { - return switch (target) { - case ParsedType.App(var fun, var arg) -> Lists.concat(findRules(fun), findRules(arg)); - case ParsedType.Const(var java) -> rulesOf(java); - case ParsedType.Var(var java) -> List.of(); - case ParsedType.ArrayOf(var elem) -> List.of(); - case ParsedType.Primitive(var java) -> List.of(); - }; - } - - private static List rulesOf(Class cls) { - return Arrays.stream(cls.getDeclaredMethods()) - .filter(TypeClasses::isWitnessMethod) - .map(FuncType::parse) - .map(InstanceConstructor::new) - .toList(); - } - - private static boolean isWitnessMethod(Method m) { - return m.accessFlags().contains(PUBLIC) - && m.accessFlags().contains(STATIC) - && m.isAnnotationPresent(TypeClass.Witness.class); - } - - private sealed interface WitnessRule { - Maybe> tryMatch(ParsedType target); - - Object instantiate(List dependencies); - } - - private record ContextInstance(Object instance, ParsedType type) implements WitnessRule { - @Override - public Maybe> tryMatch(ParsedType target) { - return target.equals(type) ? Maybe.just(List.of()) : Maybe.nothing(); - } - - @Override - public Object instantiate(List dependencies) { - return instance; - } - - @Override - public String toString() { - return "context instance: " + type.format(); - } - } - - private record InstanceConstructor(FuncType func) implements WitnessRule { - public TypeClass.Witness.Overlap overlap() { - return func.java().getAnnotation(TypeClass.Witness.class).overlap(); - } - - @Override - public Maybe> tryMatch(ParsedType target) { - return Unification.unify(func.returnType(), target) - .map(map -> Unification.substituteAll(map, func.paramTypes())); - } - - @Override - public Object instantiate(List dependencies) { - try { - return func.java().invoke(null, dependencies.toArray()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public String toString() { - return func.format(); - } - } -} +// Internal witness resolution utilities +// TypeClasses is now in com.garciat.typeclasses.TypeClasses // === First-Order Type Classes === @@ -625,12 +331,6 @@ static Eq> mapEq(Eq eqK, Eq eqV) { } } -enum Ordering { - LT, - EQ, - GT -} - @TypeClass interface Ord extends Eq { Ordering compare(A a1, A a2); @@ -1726,8 +1426,15 @@ static Parser unwrap(TApp value) { } } -// === Weird Type Class Examples === +// === Example Type Class Implementations === +// These are example implementations demonstrating advanced type class features. +// They are package-private as they're primarily for demonstration purposes. +/** + * Example type class demonstrating variadic functions through type class resolution. + * + * @param the result type + */ @TypeClass interface SumAllInt { A sum(List list); @@ -1764,7 +1471,10 @@ static SumAllInt> func3( } /** - * @implNote Source + * Example type class for variadic printing. + * + * @param the result type + * @see Source */ @TypeClass interface PrintAll { @@ -1806,6 +1516,7 @@ static PrintAll> func3( } } +/** Helper interface for one-argument functions used in examples. */ @FunctionalInterface interface F1 { R apply(A a); @@ -1815,6 +1526,7 @@ static F1 of(Function f) { } } +/** Helper interface for two-argument functions used in examples. */ @FunctionalInterface interface F2 { R apply(A a, B b); @@ -1824,6 +1536,7 @@ static F2 of(Function> f) { } } +/** Helper interface for three-argument functions used in examples. */ @FunctionalInterface interface F3 { R apply(A a, B b, C c); @@ -1834,53 +1547,4 @@ static F3 of(Function>> f } // === Utilities === - -sealed interface ZeroOneMore { - record Zero() implements ZeroOneMore {} - - record One(A value) implements ZeroOneMore {} - - record More(List values) implements ZeroOneMore {} - - static ZeroOneMore of(List list) { - return switch (list.size()) { - case 0 -> new Zero<>(); - case 1 -> new One<>(list.getFirst()); - default -> new More<>(list); - }; - } -} - -class Lists { - public static List map(List list, Function f) { - return list.stream().map(f).collect(Collectors.toList()); - } - - @SafeVarargs - public static List concat(List... lists) { - return Arrays.stream(lists).flatMap(List::stream).toList(); - } -} - -class Maps { - public static Map merge(Map m1, Map m2) { - Map result = new HashMap<>(m1); - for (Map.Entry entry : m2.entrySet()) { - V existing = result.put(entry.getKey(), entry.getValue()); - if (existing != null && !existing.equals(entry.getValue())) { - throw new IllegalArgumentException("Duplicate key: " + entry.getKey()); - } - } - return result; - } -} - -class Functions { - public static BiFunction flip(BiFunction f) { - return (b, a) -> f.apply(a, b); - } - - public static Function> curry(BiFunction f) { - return a -> b -> f.apply(a, b); - } -} +// ZeroOneMore, Lists, Maps, Functions are now in com.garciat.typeclasses.impl package diff --git a/src/main/java/com/garciat/typeclasses/api/Ctx.java b/src/main/java/com/garciat/typeclasses/api/Ctx.java new file mode 100644 index 0000000..7125940 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/api/Ctx.java @@ -0,0 +1,22 @@ +package com.garciat.typeclasses.api; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Objects; + +public abstract class Ctx { + private final T instance; + + public Ctx(T instance) { + this.instance = instance; + } + + public T instance() { + return instance; + } + + public Type type() { + return Objects.requireNonNull( + ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]); + } +} diff --git a/src/main/java/com/garciat/typeclasses/api/Ty.java b/src/main/java/com/garciat/typeclasses/api/Ty.java new file mode 100644 index 0000000..6c2988f --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/api/Ty.java @@ -0,0 +1,12 @@ +package com.garciat.typeclasses.api; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Objects; + +public interface Ty { + default Type type() { + return Objects.requireNonNull( + ((ParameterizedType) getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0]); + } +} diff --git a/src/main/java/com/garciat/typeclasses/api/TypeClass.java b/src/main/java/com/garciat/typeclasses/api/TypeClass.java new file mode 100644 index 0000000..c264b03 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/api/TypeClass.java @@ -0,0 +1,18 @@ +package com.garciat.typeclasses.api; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeClass { + @Retention(RetentionPolicy.RUNTIME) + @interface Witness { + Overlap overlap() default Overlap.NONE; + + enum Overlap { + NONE, + OVERLAPPING, + OVERLAPPABLE + } + } +} diff --git a/src/main/java/com/garciat/typeclasses/api/hkt/Kind.java b/src/main/java/com/garciat/typeclasses/api/hkt/Kind.java new file mode 100644 index 0000000..9f62ff6 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/api/hkt/Kind.java @@ -0,0 +1,9 @@ +package com.garciat.typeclasses.api.hkt; + +public interface Kind { + sealed interface Base permits KStar, KArr {} + + final class KStar implements Base {} + + final class KArr implements Base {} +} diff --git a/src/main/java/com/garciat/typeclasses/api/hkt/TApp.java b/src/main/java/com/garciat/typeclasses/api/hkt/TApp.java new file mode 100644 index 0000000..1292ceb --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/api/hkt/TApp.java @@ -0,0 +1,3 @@ +package com.garciat.typeclasses.api.hkt; + +public interface TApp>, A> extends Kind {} diff --git a/src/main/java/com/garciat/typeclasses/api/hkt/TPar.java b/src/main/java/com/garciat/typeclasses/api/hkt/TPar.java new file mode 100644 index 0000000..3f383e3 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/api/hkt/TPar.java @@ -0,0 +1,4 @@ +package com.garciat.typeclasses.api.hkt; + +public interface TPar>>, A> + extends Kind> {} diff --git a/src/main/java/com/garciat/typeclasses/api/hkt/TagBase.java b/src/main/java/com/garciat/typeclasses/api/hkt/TagBase.java new file mode 100644 index 0000000..324ca28 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/api/hkt/TagBase.java @@ -0,0 +1,3 @@ +package com.garciat.typeclasses.api.hkt; + +public abstract class TagBase implements Kind {} diff --git a/src/main/java/com/garciat/typeclasses/classes/Ordering.java b/src/main/java/com/garciat/typeclasses/classes/Ordering.java new file mode 100644 index 0000000..48b261d --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/Ordering.java @@ -0,0 +1,7 @@ +package com.garciat.typeclasses.classes; + +public enum Ordering { + LT, + EQ, + GT +} diff --git a/src/main/java/com/garciat/typeclasses/impl/Functions.java b/src/main/java/com/garciat/typeclasses/impl/Functions.java new file mode 100644 index 0000000..c85c9d6 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/impl/Functions.java @@ -0,0 +1,15 @@ +package com.garciat.typeclasses.impl; + +import java.util.function.BiFunction; +import java.util.function.Function; + +/** Internal function utilities. */ +public class Functions { + public static BiFunction flip(BiFunction f) { + return (b, a) -> f.apply(a, b); + } + + public static Function> curry(BiFunction f) { + return a -> b -> f.apply(a, b); + } +} diff --git a/src/main/java/com/garciat/typeclasses/impl/Lists.java b/src/main/java/com/garciat/typeclasses/impl/Lists.java new file mode 100644 index 0000000..2ae2324 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/impl/Lists.java @@ -0,0 +1,18 @@ +package com.garciat.typeclasses.impl; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** Internal list utilities. */ +public class Lists { + public static List map(List list, Function f) { + return list.stream().map(f).collect(Collectors.toList()); + } + + @SafeVarargs + public static List concat(List... lists) { + return Arrays.stream(lists).flatMap(List::stream).toList(); + } +} diff --git a/src/main/java/com/garciat/typeclasses/impl/Maps.java b/src/main/java/com/garciat/typeclasses/impl/Maps.java new file mode 100644 index 0000000..a1313c9 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/impl/Maps.java @@ -0,0 +1,18 @@ +package com.garciat.typeclasses.impl; + +import java.util.HashMap; +import java.util.Map; + +/** Internal map utilities. */ +public class Maps { + public static Map merge(Map m1, Map m2) { + Map result = new HashMap<>(m1); + for (Map.Entry entry : m2.entrySet()) { + V existing = result.put(entry.getKey(), entry.getValue()); + if (existing != null && !existing.equals(entry.getValue())) { + throw new IllegalArgumentException("Duplicate key: " + entry.getKey()); + } + } + return result; + } +} diff --git a/src/main/java/com/garciat/typeclasses/impl/ZeroOneMore.java b/src/main/java/com/garciat/typeclasses/impl/ZeroOneMore.java new file mode 100644 index 0000000..ae17241 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/impl/ZeroOneMore.java @@ -0,0 +1,20 @@ +package com.garciat.typeclasses.impl; + +import java.util.List; + +/** Internal result type for instance selection. */ +public sealed interface ZeroOneMore { + record Zero() implements ZeroOneMore {} + + record One(A value) implements ZeroOneMore {} + + record More(List values) implements ZeroOneMore {} + + static ZeroOneMore of(List list) { + return switch (list.size()) { + case 0 -> new Zero<>(); + case 1 -> new One<>(list.getFirst()); + default -> new More<>(list); + }; + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..3001540 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,6 @@ +module com.garciat.typeclasses { + exports com.garciat.typeclasses.api; + exports com.garciat.typeclasses.api.hkt; + exports com.garciat.typeclasses.classes; + exports com.garciat.typeclasses; +} diff --git a/src/test/java/com/garciat/typeclasses/ExamplesTest.java b/src/test/java/com/garciat/typeclasses/ExamplesTest.java new file mode 100644 index 0000000..8fe6df5 --- /dev/null +++ b/src/test/java/com/garciat/typeclasses/ExamplesTest.java @@ -0,0 +1,74 @@ +package com.garciat.typeclasses; + +import static com.garciat.typeclasses.TypeClasses.witness; + +import com.garciat.typeclasses.api.Ctx; +import com.garciat.typeclasses.api.Ty; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import org.junit.jupiter.api.Test; + +/** Tests demonstrating the java-type-classes library features. */ +final class ExamplesTest { + + @Test + void examples() { + System.out.println(Show.show(witness(new Ty<>() {}), new int[] {1, 2, 3, 4, 5})); + + System.out.println(Show.show(witness(new Ty<>() {}), new Integer[] {1, 2, 3, 4, 5})); + + Map>> m1 = + Map.of( + "a", + List.of(Optional.of(1), Optional.empty()), + "b", + List.of(Optional.of(2), Optional.of(3))); + + System.out.printf("show(m1) = %s\n", Show.show(witness(new Ty<>() {}), m1)); + + List> sums = List.of(new Sum<>(3), new Sum<>(5), new Sum<>(10)); + + System.out.printf( + "combineAll(%s) = %s\n", sums, Monoid.combineAll(witness(new Ty<>() {}), sums)); + + System.out.printf("eq(m1, m1) = %s\n", Eq.eq(witness(new Ty<>() {}), m1, m1)); + + Optional m5 = Optional.of(5); + Optional m10 = Optional.of(10); + + System.out.printf( + "compare(%s, %s) = %s\n", m5, m10, Ord.compare(witness(new Ty<>() {}), m5, m10)); + + Arbitrary, List>>> arbFunc = + witness(new Ty<>() {}); + var f = arbFunc.arbitrary().generate(42L, 10); + + System.out.println("f(10) = " + f.apply(Optional.of(5))); + + System.out.println( + Traversable.traverse( + witness(new Ty<>() {}), witness(new Ty<>() {}), JavaList.of(1, 2, 3), Maybe::just)); + + System.out.println(Show.show(witness(new Ty<>() {}), FwdList.of('h', 'e', 'l', 'l', 'o'))); + + example(witness(new Ty<>() {}), 123); + + F3 sum = SumAllInt.of(witness(new Ty<>() {})); + System.out.println(sum.apply(1, 2, 3)); + + F3, Integer, Void> printer = PrintAll.of(witness(new Ty<>() {})); + printer.apply("Items:", JavaList.of("apple", "banana", "cherry"), 0); + + Foldable foldableFwdList = witness(new Ty<>() {}); + + System.out.println(foldableFwdList.length(FwdList.of(1, 2, 3, 4, 5))); + + System.out.println(foldableFwdList.toList(FwdList.of(1, 2, 3))); + } + + static void example(Show showA, A value) { + System.out.println(Show.show(witness(new Ty<>() {}, new Ctx<>(showA) {}), JavaList.of(value))); + } +}