diff --git a/src/main/java/com/garciat/typeclasses/Main.java b/src/main/java/com/garciat/typeclasses/Main.java deleted file mode 100644 index 706fc5f..0000000 --- a/src/main/java/com/garciat/typeclasses/Main.java +++ /dev/null @@ -1,1886 +0,0 @@ -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 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 java.lang.reflect.GenericArrayType; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; -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; -import java.util.function.BiFunction; -import java.util.function.BinaryOperator; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; -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> {} - -sealed interface ParsedType { - record Var(TypeVariable java) implements ParsedType {} - - record App(ParsedType fun, ParsedType arg) implements ParsedType {} - - record ArrayOf(ParsedType elementType) implements ParsedType {} - - record Const(Class java) implements ParsedType {} - - record Primitive(Class java) implements ParsedType {} - - default String format() { - return switch (this) { - case Var v -> v.java.getName(); - case Const c -> - c.java().getSimpleName() - + Arrays.stream(c.java().getTypeParameters()) - .map(TypeVariable::getName) - .reduce((a, b) -> a + ", " + b) - .map(s -> "[" + s + "]") - .orElse(""); - case App a -> a.fun.format() + "(" + a.arg.format() + ")"; - case ArrayOf a -> a.elementType.format() + "[]"; - case Primitive p -> p.java().getSimpleName(); - }; - } - - static List parseAll(Type[] types) { - return Arrays.stream(types).map(ParsedType::parse).toList(); - } - - static ParsedType parse(Type java) { - return switch (java) { - case Class tag when parseTagType(tag) instanceof Maybe.Just>(var tagged) -> - new Const(tagged); - case Class arr when arr.isArray() -> new ArrayOf(parse(arr.getComponentType())); - case Class prim when prim.isPrimitive() -> new Primitive(prim); - case Class c -> new Const(c); - case TypeVariable v -> new Var(v); - case ParameterizedType p - when parseAppType(p) - instanceof Maybe.Just>(Pair(var fun, var arg)) -> - new App(parse(fun), parse(arg)); - case ParameterizedType p -> - parseAll(p.getActualTypeArguments()).stream().reduce(parse(p.getRawType()), App::new); - case GenericArrayType a -> new ArrayOf(parse(a.getGenericComponentType())); - case WildcardType w -> throw new IllegalArgumentException("Cannot parse wildcard type: " + w); - default -> throw new IllegalArgumentException("Unsupported type: " + java); - }; - } - - private static Maybe> parseTagType(Class c) { - return switch (c.getEnclosingClass()) { - case Class enclosing when c.getSuperclass().equals(TagBase.class) -> Maybe.just(enclosing); - case null -> Maybe.nothing(); - default -> Maybe.nothing(); - }; - } - - private static Maybe> parseAppType(ParameterizedType t) { - return switch (t.getRawType()) { - case Class raw when raw.equals(TApp.class) || raw.equals(TPar.class) -> - Maybe.just(Pair.of(t.getActualTypeArguments()[0], t.getActualTypeArguments()[1])); - default -> Maybe.nothing(); - }; - } -} - -class Unification { - public static Maybe> unify(ParsedType t1, ParsedType t2) { - return switch (Pair.of(t1, t2)) { - case Pair(ParsedType.Var var1, ParsedType.Primitive p) -> - Maybe.nothing(); // no primitives in generics - case Pair(ParsedType.Var var1, var t) -> Maybe.just(Map.of(var1, t)); - case Pair(ParsedType.Const const1, ParsedType.Const const2) - when const1.equals(const2) -> - Maybe.just(Map.of()); - case Pair( - ParsedType.App(var fun1, var arg1), - ParsedType.App(var fun2, var arg2)) -> - Maybe.apply(Maps::merge, unify(fun1, fun2), unify(arg1, arg2)); - case Pair( - ParsedType.ArrayOf(var elem1), - ParsedType.ArrayOf(var elem2)) -> - unify(elem1, elem2); - case Pair( - ParsedType.Primitive(var prim1), - ParsedType.Primitive(var prim2)) - when prim1.equals(prim2) -> - Maybe.just(Map.of()); - default -> Maybe.nothing(); - }; - } - - public static ParsedType substitute(Map map, ParsedType type) { - return switch (type) { - case ParsedType.Var var -> map.getOrDefault(var, var); - case ParsedType.App(var fun, var arg) -> - new ParsedType.App(substitute(map, fun), substitute(map, arg)); - case ParsedType.ArrayOf var -> new ParsedType.ArrayOf(substitute(map, var.elementType())); - case ParsedType.Primitive p -> p; - case ParsedType.Const c -> c; - }; - } - - public static List substituteAll( - Map map, List types) { - return types.stream().map(t -> substitute(map, t)).toList(); - } -} - -record FuncType(Method java, List paramTypes, ParsedType returnType) { - public String format() { - return String.format( - "%s%s -> %s", - Arrays.stream(java.getTypeParameters()) - .map(TypeVariable::getName) - .reduce((a, b) -> a + " " + b) - .map("∀ %s. "::formatted) - .orElse(""), - paramTypes.stream().map(ParsedType::format).collect(Collectors.joining(", ", "(", ")")), - returnType.format()); - } - - public static FuncType parse(Method method) { - if (!Modifier.isStatic(method.getModifiers())) { - throw new IllegalArgumentException("Method must be static: " + method); - } - return new FuncType( - method, - ParsedType.parseAll(method.getGenericParameterTypes()), - ParsedType.parse(method.getGenericReturnType())); - } -} - -// === Type Class System === - -@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(); - } - } -} - -// === First-Order Type Classes === - -@TypeClass -sealed interface TyEq { - A castR(B b); - - B castL(A a); - - static TyEq refl() { - return new Refl<>(); - } - - record Refl() implements TyEq { - - @Override - public T castR(T t) { - return t; - } - - @Override - public T castL(T t) { - return t; - } - } - - @TypeClass.Witness - static TyEq tyEqRefl() { - return refl(); - } -} - -@TypeClass -interface Show { - String show(A a); - - static String show(Show showA, A a) { - return showA.show(a); - } - - @TypeClass.Witness - static Show integerShow() { - return i -> Integer.toString(i); - } - - @TypeClass.Witness - static Show stringShow() { - return s -> "\"" + s + "\""; - } - - @TypeClass.Witness - static Show> optionalShow(Show showA) { - return optA -> optA.map(a -> "Some(" + showA.show(a) + ")").orElse("None"); - } - - @TypeClass.Witness - static Show arrayShow(Show showA) { - return arrayA -> - Arrays.stream(arrayA).map(showA::show).collect(Collectors.joining(", ", "[", "]")); - } - - @TypeClass.Witness - static Show intArrayShow() { - return arrayA -> - Arrays.stream(arrayA) - .mapToObj(Integer::toString) - .collect(Collectors.joining(", ", "[", "]")); - } - - @TypeClass.Witness - static Show> listShow(Show showA) { - return listA -> listA.stream().map(showA::show).collect(Collectors.joining(", ", "[", "]")); - } - - @TypeClass.Witness - static Show> mapShow(Show showK, Show showV) { - return mapKV -> - mapKV.entrySet().stream() - .map(entry -> showK.show(entry.getKey()) + ": " + showV.show(entry.getValue())) - .collect(Collectors.joining(", ", "{", "}")); - } -} - -@TypeClass -interface Eq { - boolean eq(A a1, A a2); - - static boolean eq(Eq eqA, A a1, A a2) { - return eqA.eq(a1, a2); - } - - @TypeClass.Witness - static Eq integerEq() { - return Integer::equals; - } - - @TypeClass.Witness - static Eq stringEq() { - return String::equals; - } - - @TypeClass.Witness - static Eq> optionalEq(Eq eqA) { - return (optA1, optA2) -> - optA1.isPresent() && optA2.isPresent() - ? eqA.eq(optA1.get(), optA2.get()) - : optA1.isEmpty() && optA2.isEmpty(); - } - - @TypeClass.Witness - static Eq> listEq(Eq eqA) { - return (listA1, listA2) -> { - if (listA1.size() != listA2.size()) { - return false; - } - for (int i = 0; i < listA1.size(); i++) { - if (!eqA.eq(listA1.get(i), listA2.get(i))) { - return false; - } - } - return true; - }; - } - - @TypeClass.Witness - static Eq> mapEq(Eq eqK, Eq eqV) { - return (map1, map2) -> { - if (map1.size() != map2.size()) { - return false; - } - for (Map.Entry entry1 : map1.entrySet()) { - boolean found = false; - for (Map.Entry entry2 : map2.entrySet()) { - if (eqK.eq(entry1.getKey(), entry2.getKey()) - && eqV.eq(entry1.getValue(), entry2.getValue())) { - found = true; - break; - } - } - if (!found) { - return false; - } - } - return true; - }; - } -} - -enum Ordering { - LT, - EQ, - GT -} - -@TypeClass -interface Ord extends Eq { - Ordering compare(A a1, A a2); - - @Override - default boolean eq(A a1, A a2) { - return compare(a1, a2) == Ordering.EQ; - } - - static Ordering compare(Ord ordA, A a1, A a2) { - return ordA.compare(a1, a2); - } - - static boolean lt(Ord ordA, A a1, A a2) { - return ordA.compare(a1, a2) == Ordering.LT; - } - - @TypeClass.Witness - static Ord integerOrd() { - return (a1, a2) -> a1 < a2 ? Ordering.LT : a1 > a2 ? Ordering.GT : Ordering.EQ; - } - - @TypeClass.Witness - static Ord> optionalOrd(Ord ordA) { - return (optA1, optA2) -> { - if (optA1.isPresent() && optA2.isPresent()) { - return ordA.compare(optA1.get(), optA2.get()); - } else if (optA1.isEmpty() && optA2.isEmpty()) { - return Ordering.EQ; - } else if (optA1.isEmpty()) { - return Ordering.LT; - } else { - return Ordering.GT; - } - }; - } -} - -@TypeClass -interface Monoid { - A combine(A a1, A a2); - - A identity(); - - static A combineAll(Monoid monoid, List elements) { - A result = monoid.identity(); - for (A element : elements) { - result = monoid.combine(result, element); - } - return result; - } - - static Monoid of(Supplier identity, BinaryOperator combine) { - return new Monoid<>() { - @Override - public A combine(A a1, A a2) { - return combine.apply(a1, a2); - } - - @Override - public A identity() { - return identity.get(); - } - }; - } - - @TypeClass.Witness - static Monoid stringMonoid() { - return new Monoid<>() { - @Override - public String combine(String s1, String s2) { - return s1 + s2; - } - - @Override - public String identity() { - return ""; - } - }; - } -} - -@TypeClass -interface Num { - A add(A a1, A a2); - - A mul(A a1, A a2); - - A zero(); - - A one(); - - @TypeClass.Witness - static Num integerNum() { - return new Num<>() { - @Override - public Integer add(Integer a1, Integer a2) { - return a1 + a2; - } - - @Override - public Integer mul(Integer a1, Integer a2) { - return a1 * a2; - } - - @Override - public Integer zero() { - return 0; - } - - @Override - public Integer one() { - return 1; - } - }; - } -} - -@TypeClass -interface RandomGen { - Pair next(G gen); - - Pair split(G gen); - - @TypeClass.Witness - static RandomGen javaUtilRandomGen() { - return new RandomGen<>() { - @Override - public Pair next(java.util.Random gen) { - return Pair.of(gen.nextInt(), gen); - } - - @Override - public Pair split(java.util.Random gen) { - java.util.Random gen1 = new java.util.Random(gen.nextLong()); - java.util.Random gen2 = new java.util.Random(gen.nextLong()); - return Pair.of(gen1, gen2); - } - }; - } -} - -@TypeClass -interface Random { - Pair random(RandomGen randomGen, G gen); - - @TypeClass.Witness - static Random integerRandom() { - return new Random<>() { - @Override - public Pair random(RandomGen randomGen, G gen) { - return randomGen.next(gen); - } - }; - } -} - -@TypeClass -interface Arbitrary { - Gen arbitrary(); - - @TypeClass.Witness - static Arbitrary integerArbitrary() { - return () -> Gen.chooseInt(Integer.MIN_VALUE, Integer.MAX_VALUE); - } - - @TypeClass.Witness - static Arbitrary> optionalArbitrary(Arbitrary arbA) { - return () -> { - Gen genA = arbA.arbitrary(); - return (seed, size) -> { - Gen genBool = Gen.chooseInt(0, 2); - if (genBool.generate(seed, size) == 0) { - return Optional.of(genA.generate(seed + 1, size)); - } else { - return Optional.empty(); - } - }; - }; - } - - @TypeClass.Witness - static Arbitrary> listArbitrary(Arbitrary arbA) { - return () -> arbA.arbitrary().listOf(); - } - - @TypeClass.Witness - static Arbitrary> functionArbitrary( - CoArbitrary coarb, Arbitrary arbB) { - return () -> { - Gen genB = arbB.arbitrary(); - return (seed, size) -> a -> coarb.coarbitrary(a, genB).generate(seed, size); - }; - } -} - -@TypeClass -interface CoArbitrary { - Gen coarbitrary(A a, Gen genB); - - @TypeClass.Witness - static CoArbitrary integerCoArbitrary() { - return new CoArbitrary<>() { - @Override - public Gen coarbitrary(Integer a, Gen genB) { - return genB.variant(a); - } - }; - } - - @TypeClass.Witness - static CoArbitrary> optionalCoArbitrary(CoArbitrary coarbA) { - return new CoArbitrary<>() { - @Override - public Gen coarbitrary(Optional optA, Gen genB) { - if (optA.isPresent()) { - return coarbA.coarbitrary(optA.get(), genB).variant(1); - } else { - return genB.variant(0); - } - } - }; - } - - @TypeClass.Witness - static CoArbitrary> listCoArbitrary(CoArbitrary coarbA) { - return new CoArbitrary<>() { - @Override - public Gen coarbitrary(List listA, Gen genB) { - Gen resultGen = genB.variant(listA.size()); - for (A a : listA) { - resultGen = coarbA.coarbitrary(a, resultGen).variant(1); - } - return resultGen; - } - }; - } - - @TypeClass.Witness - static CoArbitrary> functionCoArbitrary( - Arbitrary arbA, CoArbitrary coarbB) { - return new CoArbitrary<>() { - @Override - public Gen coarbitrary(Function f, Gen genC) { - return Arbitrary.listArbitrary(arbA) - .arbitrary() - .flatMap(xs -> CoArbitrary.listCoArbitrary(coarbB).coarbitrary(Lists.map(xs, f), genC)); - } - }; - } -} - -// === Higher-Kinded Type Classes === - -record Endo(Function appEndo) { - public Endo compose(Endo other) { - return new Endo<>(a -> appEndo.apply(other.appEndo.apply(a))); - } - - public static Endo id() { - return new Endo<>(a -> a); - } - - public static Endo of(Function f) { - return new Endo<>(f); - } - - public static Function> of(BiFunction f) { - return a -> new Endo<>(b -> f.apply(a, b)); - } - - @TypeClass.Witness - static Monoid> monoid() { - return Monoid.of(Endo::id, Endo::compose); - } -} - -record Dual(A getDual) { - public static Dual of(A a) { - return new Dual<>(a); - } - - public static Monoid> monoid(Monoid monoidA) { - return Monoid.of( - () -> new Dual<>(monoidA.identity()), - (d1, d2) -> new Dual<>(monoidA.combine(d2.getDual, d1.getDual))); - } -} - -// class Foldable t where -@TypeClass -interface Foldable>> { - // foldMap :: Monoid m => (a -> m) -> t a -> m - M foldMap(Monoid monoid, Function f, TApp ta); - - // fold :: Monoid m => t m -> m - default A fold(TApp ta, Monoid monoid) { - return foldMap(monoid, identity(), ta); - } - - // foldr :: (a -> b -> b) -> b -> t a -> b - // foldr f z t = appEndo (foldMap (Endo . f) t) z - default B foldr(BiFunction f, B z, TApp t) { - Endo endo = foldMap(Endo.monoid(), curry(f).andThen(Endo::of), t); - return endo.appEndo().apply(z); - } - - // foldl :: (b -> a -> b) -> b -> t a -> b - // foldl f z t = appEndo (getDual (foldMap (Dual . Endo . flip f) t)) z - default B foldl(BiFunction f, B z, TApp t) { - Dual> dualEndo = - foldMap(Dual.monoid(Endo.monoid()), curry(flip(f)).andThen(Endo::of).andThen(Dual::of), t); - return dualEndo.getDual().appEndo().apply(z); - } - - // toList :: t a -> [a] - default FwdList toList(TApp ta) { - return FwdList.build( - new FwdList.Builder() { - @Override - public B apply(BiFunction cons, Supplier nil) { - return foldr(cons, nil.get(), ta); - } - }); - } - - // null :: t a -> Bool - default boolean isEmpty(TApp ta) { - return foldr((a, b) -> false, true, ta); - } - - // length :: t a -> Int - default int length(TApp ta) { - return foldl((n, a) -> n + 1, 0, ta); - } -} - -@TypeClass -interface Traversable>> extends Functor, Foldable { - >, A, B> TApp> traverse( - Applicative applicative, Function> f, TApp ta); - - static >, T extends Kind>, A, B> - TApp> traverse( - Traversable traversable, - Applicative applicative, - TApp tA, - Function> f) { - return traversable.traverse(applicative, f, tA); - } -} - -@TypeClass -interface Functor>> { - TApp map(Function f, TApp fa); -} - -@TypeClass -interface Applicative>> extends Functor { - TApp pure(A a); - - TApp ap(TApp> ff, TApp fa); - - @Override - default TApp map(Function f, TApp fa) { - return ap(pure(f), fa); - } - - default BiFunction, TApp, TApp> lift(BiFunction f) { - return (fa, fb) -> ap(ap(pure(a -> b -> f.apply(a, b)), fa), fb); - } - - default TApp> sequence(FwdList> fas) { - return fas.traverse(this, identity()); - } - - default TApp> sequence(JavaList> fas) { - return fas.traverse(this, identity()); - } -} - -@TypeClass -interface Alternative>> extends Applicative { - TApp empty(); - - TApp alt(TApp fa1, TApp fa2); -} - -@TypeClass -interface Monad>> extends Applicative { - TApp flatMap(Function> f, TApp fa); - - @Override - default TApp map(Function f, TApp fa) { - return flatMap(a -> pure(f.apply(a)), fa); - } - - @Override - default TApp ap(TApp> ff, TApp fa) { - return flatMap(a -> map(f -> f.apply(a), ff), fa); - } -} - -// === Functional Types === - -record JavaList(List toList) implements TApp { - - public JavaList map(Function f) { - return new JavaList<>(toList().stream().map(f).toList()); - } - - public JavaList flatMap(Function> f) { - List result = new ArrayList<>(); - for (A item : toList()) { - result.addAll(f.apply(item).toList()); - } - return new JavaList<>(result); - } - - public M foldMap(Monoid monoid, Function f) { - return toList().stream().map(f).reduce(monoid.identity(), monoid::combine); - } - - public >, B> TApp> traverse( - Applicative applicative, Function> f) { - TApp> result = applicative.pure(JavaList.of()); - for (A item : toList()) { - TApp fb = f.apply(item); - result = - applicative - .lift((JavaList bs, B b) -> JavaList.of(Lists.concat(bs.toList(), List.of(b)))) - .apply(result, fb); - } - return result; - } - - public static JavaList of() { - return new JavaList<>(List.of()); - } - - @SafeVarargs - public static JavaList of(T... items) { - return new JavaList<>(List.of(items)); - } - - public static JavaList of(List list) { - return new JavaList<>(list); - } - - @TypeClass.Witness - public static Show> show(Show showA) { - return listA -> - listA.toList().stream().map(showA::show).collect(Collectors.joining(", ", "[", "]")); - } - - @TypeClass.Witness - public static Functor functor() { - return new Control(); - } - - @TypeClass.Witness - public static Traversable traversable() { - return new Control(); - } - - private static final class Control - implements Functor, Foldable, Traversable { - @Override - public TApp map(Function f, TApp fa) { - return unwrap(fa).map(f); - } - - @Override - public M foldMap(Monoid monoid, Function f, TApp ta) { - return unwrap(ta).foldMap(monoid, f); - } - - @Override - public >, A, B> TApp> traverse( - Applicative applicative, Function> f, TApp ta) { - return unwrap(ta).traverse(applicative, f); - } - } - - public static JavaList unwrap(TApp x) { - return (JavaList) x; - } - - public static final class Tag extends TagBase> {} -} - -record Sum(A value) { - @TypeClass.Witness - public static Monoid> monoid(Num num) { - return new Monoid<>() { - @Override - public Sum combine(Sum s1, Sum s2) { - return new Sum<>(num.add(s1.value(), s2.value())); - } - - @Override - public Sum identity() { - return new Sum<>(num.zero()); - } - }; - } -} - -@FunctionalInterface -interface Gen { - A generate(long seed, int size); - - default Gen map(Function f) { - return (seed, size) -> f.apply(generate(seed, size)); - } - - // TODO: This is a naive implementation; in a real implementation, the seed - // management would be - // more sophisticated. - default Gen flatMap(Function> f) { - return (seed, size) -> f.apply(generate(seed, size)).generate(seed + 1, size); - } - - default Gen variant(int n) { - return (seed, size) -> generate(seed + n, size); - } - - default Gen> listOf() { - return sized(size -> chooseInt(0, size).flatMap(this::vectorOf)); - } - - default Gen> vectorOf(int length) { - return (seed, size) -> { - List result = new ArrayList<>(); - for (int i = 0; i < length; i++) { - result.add(generate(seed + i, size)); - } - return result; - }; - } - - static Gen chooseInt(int low, int high) { - return (seed, size) -> new java.util.Random(seed).nextInt(low, high); - } - - static Gen sized(Function> gen) { - return (seed, size) -> gen.apply(size).generate(seed, size); - } -} - -record Pair(A fst, B snd) { - - public Pair mapFst(Function f) { - return Pair.of(f.apply(fst), snd); - } - - public Pair mapSnd(Function f) { - return Pair.of(fst, f.apply(snd)); - } - - public static Pair of(A first, B second) { - return new Pair<>(first, second); - } -} - -sealed interface Either extends TApp, R> { - record Left(L value) implements Either {} - - record Right(R value) implements Either {} - - static Either left(L value) { - return new Left<>(value); - } - - static Either right(R value) { - return new Right<>(value); - } - - default Either map(Function f) { - return fold(Either::left, f.andThen(Either::right)); - } - - default Either mapLeft(Function f) { - return fold(f.andThen(Either::left), Either::right); - } - - default Either flatMap(Function> f) { - return fold(Either::left, f); - } - - default A fold( - Function fLeft, Function fRight) { - return switch (this) { - case Left(L value) -> fLeft.apply(value); - case Right(R value) -> fRight.apply(value); - }; - } - - static Either> traverse(List list, Function> f) { - return unwrap(JavaList.of(list).traverse(Either.applicative(), f::apply)).map(JavaList::toList); - } - - @TypeClass.Witness - static Functor> functor() { - return new Functor<>() { - @Override - public TApp, B> map(Function f, TApp, A> fa) { - return unwrap(fa).map(f); - } - }; - } - - @TypeClass.Witness - static Applicative> applicative() { - return monad(); - } - - @TypeClass.Witness - static Monad> monad() { - return new Monad<>() { - @Override - public TApp, A> pure(A a) { - return right(a); - } - - @Override - public TApp, B> flatMap( - Function, B>> f, TApp, A> fa) { - return unwrap(fa).flatMap(a -> unwrap(f.apply(a))); - } - }; - } - - final class Tag extends TagBase>> {} - - static Either unwrap(TApp, R> value) { - return (Either) value; - } -} - -sealed interface Maybe extends TApp { - record Just(A value) implements Maybe {} - - record Nothing() implements Maybe {} - - static Maybe just(A value) { - return new Just<>(value); - } - - static Maybe nothing() { - return new Nothing<>(); - } - - default R fold(Supplier onNothing, Function onJust) { - return switch (this) { - case Just(A value) -> onJust.apply(value); - case Nothing() -> onNothing.get(); - }; - } - - default Maybe filter(Function predicate) { - return flatMap(a -> predicate.apply(a) ? just(a) : nothing()); - } - - default Stream stream() { - return fold(Stream::empty, Stream::of); - } - - default Maybe map(Function f) { - return fold(Maybe::nothing, a -> just(f.apply(a))); - } - - default Maybe flatMap(Function> f) { - return switch (this) { - case Just(A value) -> f.apply(value); - case Nothing() -> nothing(); - }; - } - - static BiFunction, Maybe, Maybe> lift(BiFunction f) { - return (ma, mb) -> ma.flatMap(a -> mb.map(b -> f.apply(a, b))); - } - - static Maybe apply(BiFunction f, Maybe ma, Maybe mb) { - return lift(f).apply(ma, mb); - } - - @TypeClass.Witness - static Functor functor() { - return new Functor<>() { - @Override - public TApp map(Function f, TApp fa) { - return unwrap(fa).map(f); - } - }; - } - - @TypeClass.Witness - static Applicative applicative() { - return new Applicative<>() { - @Override - public TApp pure(A a) { - return just(a); - } - - @Override - public TApp ap(TApp> ff, TApp fa) { - return unwrap(ff).flatMap(f -> unwrap(fa).map(f)); - } - }; - } - - @TypeClass.Witness - static Monad monad() { - return new Monad<>() { - @Override - public TApp pure(A a) { - return just(a); - } - - @Override - public TApp flatMap(Function> f, TApp fa) { - return unwrap(fa).flatMap(a -> unwrap(f.apply(a))); - } - }; - } - - final class Tag extends TagBase> {} - - static Maybe unwrap(TApp value) { - return (Maybe) value; - } -} - -@FunctionalInterface -interface State extends TApp, A> { - Pair run(S state); - - static State of(Function> f) { - return f::apply; - } - - static State pure(A a) { - return state -> Pair.of(a, state); - } - - default State map(Function f) { - return state -> run(state).mapFst(f); - } - - default State flatMap(Function> f) { - return state -> - switch (run(state)) { - case Pair(A a, S newState) -> f.apply(a).run(newState); - }; - } - - @TypeClass.Witness - static Functor> functor() { - return new Functor<>() { - @Override - public TApp, B> map(Function f, TApp, A> fa) { - return unwrap(fa).map(f); - } - }; - } - - @TypeClass.Witness - static Applicative> applicative() { - return new Applicative<>() { - @Override - public TApp, A> pure(A a) { - return State.pure(a); - } - - @Override - public TApp, B> ap( - TApp, Function> ff, TApp, A> fa) { - return unwrap(ff).flatMap(f -> unwrap(fa).map(f)); - } - }; - } - - @TypeClass.Witness - static Monad> monad() { - return new Monad<>() { - @Override - public TApp, A> pure(A a) { - return State.pure(a); - } - - @Override - public TApp, B> flatMap( - Function, B>> f, TApp, A> fa) { - return unwrap(fa).flatMap(a -> unwrap(f.apply(a))); - } - }; - } - - final class Tag extends TagBase>> {} - - static State unwrap(TApp, A> value) { - return (State) value; - } -} - -sealed interface FwdList extends TApp { - record Nil() implements FwdList {} - - record Cons(A head, FwdList tail) implements FwdList {} - - default R match(Supplier onNil, BiFunction, R> onCons) { - return switch (this) { - case Nil() -> onNil.get(); - case Cons(A head, FwdList tail) -> onCons.apply(head, tail); - }; - } - - default M foldMap(Monoid monoid, Function f) { - return match( - monoid::identity, (head, tail) -> monoid.combine(f.apply(head), tail.foldMap(monoid, f))); - } - - default B foldr(B identity, BiFunction f) { - return match(() -> identity, (head, tail) -> f.apply(head, tail.foldr(identity, f))); - } - - default B foldl(B identity, BiFunction f) { - return match(() -> identity, (head, tail) -> tail.foldl(f.apply(identity, head), f)); - } - - default void forEach(Consumer action) { - this.match( - () -> null, - (head, tail) -> { - action.accept(head); - tail.forEach(action); - return null; - }); - } - - default FwdList map(Function f) { - return match(FwdList::of, (head, tail) -> cons(f.apply(head), tail.map(f))); - } - - default FwdList flatMap(Function> f) { - return match(FwdList::of, (head, tail) -> append(f.apply(head), tail.flatMap(f))); - } - - default >, B> TApp> traverse( - Applicative applicative, Function> f) { - return foldr( - applicative.pure(FwdList.of()), - (head, tailT) -> - applicative.lift((B h, FwdList t) -> cons(h, t)).apply(f.apply(head), tailT)); - } - - static FwdList append(FwdList list1, FwdList list2) { - return list1.match(() -> list2, (head, tail) -> cons(head, append(tail, list2))); - } - - static FwdList of() { - return new Nil<>(); - } - - static FwdList cons(A head, FwdList tail) { - return new Cons<>(head, tail); - } - - static FwdList of(A a) { - return cons(a, of()); - } - - @SafeVarargs - static FwdList of(A... items) { - return of(Arrays.asList(items)); - } - - static FwdList of(Iterable iter) { - return unfoldr( - iter.iterator(), it -> it.hasNext() ? Maybe.just(Pair.of(it.next(), it)) : Maybe.nothing()); - } - - static FwdList of(CharSequence str) { - return unfoldr( - 0, - index -> - index < str.length() - ? Maybe.just(Pair.of(str.charAt(index), index + 1)) - : Maybe.nothing()); - } - - default String toStr(TyEq ty) { - StringBuilder sb = new StringBuilder(); - forEach(ch -> sb.append((char) ty.castL(ch))); - return sb.toString(); - } - - static FwdList unfoldr(B seed, Function>> f) { - Maybe> result = f.apply(seed); - return result.fold(FwdList::of, pair -> cons(pair.fst(), unfoldr(pair.snd(), f))); - } - - public static FwdList build(Builder builder) { - return builder.apply(FwdList::cons, FwdList::of); - } - - public interface Builder { - B apply(BiFunction cons, Supplier nil); - } - - @TypeClass.Witness - static Show> show(Show showA) { - return list -> { - StringBuilder sb = new StringBuilder(); - sb.append("["); - list.forEach(a -> sb.append(showA.show(a))); - sb.append("]"); - return sb.toString(); - }; - } - - @TypeClass.Witness(overlap = OVERLAPPING) - static Show> show() { - return list -> { - StringBuilder sb = new StringBuilder(); - sb.append("\""); - list.forEach(sb::append); - sb.append("\""); - return sb.toString(); - }; - } - - @TypeClass.Witness - static Functor functor() { - return new Control(); - } - - @TypeClass.Witness - static Foldable foldable() { - return new Control(); - } - - @TypeClass.Witness - static Traversable traversable() { - return new Control(); - } - - @TypeClass.Witness - static Applicative applicative() { - return new Control(); - } - - @TypeClass.Witness - static Monad monad() { - return new Control(); - } - - final class Control implements Functor, Applicative, Monad, Traversable { - @Override - public TApp map(Function f, TApp fa) { - return unwrap(fa).map(f); - } - - @Override - public M foldMap(Monoid monoid, Function f, TApp ta) { - return unwrap(ta).foldMap(monoid, f); - } - - @Override - public >, A, B> TApp> traverse( - Applicative applicative, Function> f, TApp ta) { - return unwrap(ta).traverse(applicative, f); - } - - @Override - public TApp pure(A a) { - return FwdList.of(a); - } - - @Override - public TApp flatMap(Function> f, TApp fa) { - return unwrap(fa).flatMap(a -> unwrap(f.apply(a))); - } - } - - final class Tag extends TagBase> {} - - static FwdList unwrap(TApp value) { - return (FwdList) value; - } -} - -@FunctionalInterface -interface Parser extends TApp { - Maybe>> parse(FwdList input); - - default Parser map(Function f) { - return input -> parse(input).map(pair -> pair.mapFst(f)); - } - - default Parser flatMap(Function> f) { - return input -> parse(input).flatMap(pair -> f.apply(pair.fst()).parse(pair.snd())); - } - - default Parser or(Parser other) { - return input -> parse(input).fold(() -> other.parse(input), Maybe::just); - } - - default Parser applyTo(Parser> pf) { - return pf.flatMap(this::map); - } - - static Parser pure(A a) { - return input -> Maybe.just(Pair.of(a, input)); - } - - static Parser fail() { - return input -> Maybe.nothing(); - } - - static Parser satisfy(Predicate predicate) { - return input -> - input.match( - () -> Maybe.nothing(), - (head, tail) -> - predicate.test(head) ? Maybe.just(Pair.of(head, tail)) : Maybe.nothing()); - } - - static Parser charParser(char c) { - return satisfy(ch -> ch == c); - } - - static Parser stringParser(String str) { - return unwrap(FwdList.of(str).traverse(applicative(), Parser::charParser)) - .map(cs -> cs.toStr(refl())); - } - - @TypeClass.Witness - static Functor functor() { - return Control.INSTANCE; - } - - @TypeClass.Witness - static Applicative applicative() { - return Control.INSTANCE; - } - - @TypeClass.Witness - static Alternative alternative() { - return Control.INSTANCE; - } - - @TypeClass.Witness - static Monad monad() { - return Control.INSTANCE; - } - - final class Control implements Monad, Alternative { - private static final Control INSTANCE = new Control(); - - @Override - public TApp pure(A a) { - return Parser.pure(a); - } - - @Override - public TApp ap(TApp> ff, TApp fa) { - return unwrap(fa).applyTo(unwrap(ff)); - } - - @Override - public TApp flatMap( - Function> f, TApp fa) { - return unwrap(fa).flatMap(a -> unwrap(f.apply(a))); - } - - @Override - public TApp empty() { - return Parser.fail(); - } - - @Override - public TApp alt(TApp fa1, TApp fa2) { - return unwrap(fa1).or(unwrap(fa2)); - } - } - - final class Tag extends TagBase> {} - - static Parser unwrap(TApp value) { - return (Parser) value; - } -} - -// === Weird Type Class Examples === - -@TypeClass -interface SumAllInt { - A sum(List list); - - static T of(SumAllInt sumAllInt) { - return sumAllInt.sum(List.of()); - } - - @TypeClass.Witness - static SumAllInt base() { - return list -> list.stream().mapToInt(Integer::intValue).sum(); - } - - @TypeClass.Witness - static SumAllInt> func(SumAllInt sumR, TyEq eq) { - return list -> a -> sumR.sum(Lists.concat(list, List.of(eq.castL(a)))); - } - - @TypeClass.Witness - static SumAllInt> func1(SumAllInt> sumR) { - return list -> F1.of(sumR.sum(list)); - } - - @TypeClass.Witness - static SumAllInt> func2(SumAllInt>> sumR) { - return list -> F2.of(sumR.sum(list)); - } - - @TypeClass.Witness - static SumAllInt> func3( - SumAllInt>>> sumR) { - return list -> F3.of(sumR.sum(list)); - } -} - -/** - * @implNote Source - */ -@TypeClass -interface PrintAll { - T printAll(List strings); - - static T of(PrintAll printAll) { - return printAll.printAll(List.of()); - } - - @TypeClass.Witness - static PrintAll base() { - return strings -> { - for (String s : strings) { - System.out.println(s); - } - return null; - }; - } - - @TypeClass.Witness - static PrintAll> func(Show showA, PrintAll printR) { - return strings -> a -> printR.printAll(Lists.concat(strings, List.of(showA.show(a)))); - } - - @TypeClass - static PrintAll> func1(PrintAll> printR) { - return strings -> F1.of(printR.printAll(strings)); - } - - @TypeClass.Witness - static PrintAll> func2(PrintAll>> printR) { - return strings -> F2.of(printR.printAll(strings)); - } - - @TypeClass.Witness - static PrintAll> func3( - PrintAll>>> printR) { - return strings -> F3.of(printR.printAll(strings)); - } -} - -@FunctionalInterface -interface F1 { - R apply(A a); - - static F1 of(Function f) { - return f::apply; - } -} - -@FunctionalInterface -interface F2 { - R apply(A a, B b); - - static F2 of(Function> f) { - return (a, b) -> f.apply(a).apply(b); - } -} - -@FunctionalInterface -interface F3 { - R apply(A a, B b, C c); - - static F3 of(Function>> f) { - return (a, b, c) -> f.apply(a).apply(b).apply(c); - } -} - -// === 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); - } -} 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..3a712fd --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/TypeClasses.java @@ -0,0 +1,205 @@ +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.FuncType; +import com.garciat.typeclasses.impl.ParsedType; +import com.garciat.typeclasses.impl.Unification; +import com.garciat.typeclasses.impl.utils.Lists; +import com.garciat.typeclasses.impl.utils.ZeroOneMore; +import com.garciat.typeclasses.types.Either; +import com.garciat.typeclasses.types.Maybe; +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/api/Ctx.java b/src/main/java/com/garciat/typeclasses/api/Ctx.java new file mode 100644 index 0000000..9384e63 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/api/Ctx.java @@ -0,0 +1,23 @@ +package com.garciat.typeclasses.api; + +import static java.util.Objects.requireNonNull; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public abstract class Ctx { + private final T instance; + + public Ctx(T instance) { + this.instance = instance; + } + + public T instance() { + return instance; + } + + public Type type() { + return 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..4cad2e2 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/api/Ty.java @@ -0,0 +1,13 @@ +package com.garciat.typeclasses.api; + +import static java.util.Objects.requireNonNull; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public interface Ty { + default Type type() { + return 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..ee53308 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/api/TypeClass.java @@ -0,0 +1,22 @@ +package com.garciat.typeclasses.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeClass { + @Target(ElementType.METHOD) + @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..1f6eb2a --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/api/hkt/Kind.java @@ -0,0 +1,12 @@ +package com.garciat.typeclasses.api.hkt; + +/** This is how we get basic kind checking in Java */ +public interface Kind { + sealed interface Base {} + + /** KStar = * */ + final class KStar implements Base {} + + /** KArr k = * -> k */ + 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..0c88515 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/api/hkt/TApp.java @@ -0,0 +1,11 @@ +package com.garciat.typeclasses.api.hkt; + +import com.garciat.typeclasses.api.hkt.Kind.KArr; +import com.garciat.typeclasses.api.hkt.Kind.KStar; + +/** + * Full application of a unary type constructor. + * + *

TApp :: (* -> *) -> * -> * + */ +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..15c0a05 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/api/hkt/TPar.java @@ -0,0 +1,11 @@ +package com.garciat.typeclasses.api.hkt; + +import com.garciat.typeclasses.api.hkt.Kind.KArr; +import com.garciat.typeclasses.api.hkt.Kind.KStar; + +/** + * Partial application of a binary type constructor. + * + *

TPar :: (* -> * -> *) -> * -> (* -> *) + */ +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/Alternative.java b/src/main/java/com/garciat/typeclasses/classes/Alternative.java new file mode 100644 index 0000000..542bbf1 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/Alternative.java @@ -0,0 +1,14 @@ +package com.garciat.typeclasses.classes; + +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; + +@TypeClass +public interface Alternative>> extends Applicative { + TApp empty(); + + TApp alt(TApp fa1, TApp fa2); +} diff --git a/src/main/java/com/garciat/typeclasses/classes/Applicative.java b/src/main/java/com/garciat/typeclasses/classes/Applicative.java new file mode 100644 index 0000000..ccdcaf6 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/Applicative.java @@ -0,0 +1,37 @@ +package com.garciat.typeclasses.classes; + +import static java.util.function.Function.identity; + +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.types.FwdList; +import com.garciat.typeclasses.types.JavaList; +import java.util.function.BiFunction; +import java.util.function.Function; + +@TypeClass +public interface Applicative>> extends Functor { + TApp pure(A a); + + TApp ap(TApp> ff, TApp fa); + + @Override + default TApp map(Function f, TApp fa) { + return ap(pure(f), fa); + } + + default BiFunction, TApp, TApp> lift(BiFunction f) { + return (fa, fb) -> ap(ap(pure(a -> b -> f.apply(a, b)), fa), fb); + } + + default TApp> sequence(FwdList> fas) { + return fas.traverse(this, identity()); + } + + default TApp> sequence(JavaList> fas) { + return fas.traverse(this, identity()); + } +} diff --git a/src/main/java/com/garciat/typeclasses/classes/Arbitrary.java b/src/main/java/com/garciat/typeclasses/classes/Arbitrary.java new file mode 100644 index 0000000..a39a685 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/Arbitrary.java @@ -0,0 +1,46 @@ +package com.garciat.typeclasses.classes; + +import com.garciat.typeclasses.api.TypeClass; +import com.garciat.typeclasses.types.Gen; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +@TypeClass +public interface Arbitrary { + Gen arbitrary(); + + @TypeClass.Witness + static Arbitrary integerArbitrary() { + return () -> Gen.chooseInt(Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + @TypeClass.Witness + static Arbitrary> optionalArbitrary(Arbitrary arbA) { + return () -> { + Gen genA = arbA.arbitrary(); + return (seed, size) -> { + Gen genBool = Gen.chooseInt(0, 2); + if (genBool.generate(seed, size) == 0) { + return Optional.of(genA.generate(seed + 1, size)); + } else { + return Optional.empty(); + } + }; + }; + } + + @TypeClass.Witness + static Arbitrary> listArbitrary(Arbitrary arbA) { + return () -> arbA.arbitrary().listOf(); + } + + @TypeClass.Witness + static Arbitrary> functionArbitrary( + CoArbitrary coarb, Arbitrary arbB) { + return () -> { + Gen genB = arbB.arbitrary(); + return (seed, size) -> a -> coarb.coarbitrary(a, genB).generate(seed, size); + }; + } +} diff --git a/src/main/java/com/garciat/typeclasses/classes/CoArbitrary.java b/src/main/java/com/garciat/typeclasses/classes/CoArbitrary.java new file mode 100644 index 0000000..22b83b8 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/CoArbitrary.java @@ -0,0 +1,64 @@ +package com.garciat.typeclasses.classes; + +import com.garciat.typeclasses.api.TypeClass; +import com.garciat.typeclasses.impl.utils.Lists; +import com.garciat.typeclasses.types.Gen; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +@TypeClass +public interface CoArbitrary { + Gen coarbitrary(A a, Gen genB); + + @TypeClass.Witness + static CoArbitrary integerCoArbitrary() { + return new CoArbitrary<>() { + @Override + public Gen coarbitrary(Integer a, Gen genB) { + return genB.variant(a); + } + }; + } + + @TypeClass.Witness + static CoArbitrary> optionalCoArbitrary(CoArbitrary coarbA) { + return new CoArbitrary<>() { + @Override + public Gen coarbitrary(Optional optA, Gen genB) { + if (optA.isPresent()) { + return coarbA.coarbitrary(optA.get(), genB).variant(1); + } else { + return genB.variant(0); + } + } + }; + } + + @TypeClass.Witness + static CoArbitrary> listCoArbitrary(CoArbitrary coarbA) { + return new CoArbitrary<>() { + @Override + public Gen coarbitrary(List listA, Gen genB) { + Gen resultGen = genB.variant(listA.size()); + for (A a : listA) { + resultGen = coarbA.coarbitrary(a, resultGen).variant(1); + } + return resultGen; + } + }; + } + + @TypeClass.Witness + static CoArbitrary> functionCoArbitrary( + Arbitrary arbA, CoArbitrary coarbB) { + return new CoArbitrary<>() { + @Override + public Gen coarbitrary(Function f, Gen genC) { + return Arbitrary.listArbitrary(arbA) + .arbitrary() + .flatMap(xs -> CoArbitrary.listCoArbitrary(coarbB).coarbitrary(Lists.map(xs, f), genC)); + } + }; + } +} diff --git a/src/main/java/com/garciat/typeclasses/classes/Eq.java b/src/main/java/com/garciat/typeclasses/classes/Eq.java new file mode 100644 index 0000000..324db68 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/Eq.java @@ -0,0 +1,71 @@ +package com.garciat.typeclasses.classes; + +import com.garciat.typeclasses.api.TypeClass; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@TypeClass +public interface Eq { + boolean eq(A a1, A a2); + + static boolean eq(Eq eqA, A a1, A a2) { + return eqA.eq(a1, a2); + } + + @TypeClass.Witness + static Eq integerEq() { + return Integer::equals; + } + + @TypeClass.Witness + static Eq stringEq() { + return String::equals; + } + + @TypeClass.Witness + static Eq> optionalEq(Eq eqA) { + return (optA1, optA2) -> + optA1.isPresent() && optA2.isPresent() + ? eqA.eq(optA1.get(), optA2.get()) + : optA1.isEmpty() && optA2.isEmpty(); + } + + @TypeClass.Witness + static Eq> listEq(Eq eqA) { + return (listA1, listA2) -> { + if (listA1.size() != listA2.size()) { + return false; + } + for (int i = 0; i < listA1.size(); i++) { + if (!eqA.eq(listA1.get(i), listA2.get(i))) { + return false; + } + } + return true; + }; + } + + @TypeClass.Witness + static Eq> mapEq(Eq eqK, Eq eqV) { + return (map1, map2) -> { + if (map1.size() != map2.size()) { + return false; + } + for (Map.Entry entry1 : map1.entrySet()) { + boolean found = false; + for (Map.Entry entry2 : map2.entrySet()) { + if (eqK.eq(entry1.getKey(), entry2.getKey()) + && eqV.eq(entry1.getValue(), entry2.getValue())) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + }; + } +} diff --git a/src/main/java/com/garciat/typeclasses/classes/Foldable.java b/src/main/java/com/garciat/typeclasses/classes/Foldable.java new file mode 100644 index 0000000..9b614e4 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/Foldable.java @@ -0,0 +1,98 @@ +package com.garciat.typeclasses.classes; + +import static com.garciat.typeclasses.impl.utils.Functions.curry; +import static com.garciat.typeclasses.impl.utils.Functions.flip; +import static java.util.function.Function.identity; + +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.types.FwdList; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +// class Foldable t where +@TypeClass +public interface Foldable>> { + // foldMap :: Monoid m => (a -> m) -> t a -> m + M foldMap(Monoid monoid, Function f, TApp ta); + + // fold :: Monoid m => t m -> m + default A fold(TApp ta, Monoid monoid) { + return foldMap(monoid, identity(), ta); + } + + // foldr :: (a -> b -> b) -> b -> t a -> b + // foldr f z t = appEndo (foldMap (Endo . f) t) z + default B foldr(BiFunction f, B z, TApp t) { + Endo endo = foldMap(Endo.monoid(), curry(f).andThen(Endo::of), t); + return endo.appEndo().apply(z); + } + + // foldl :: (b -> a -> b) -> b -> t a -> b + // foldl f z t = appEndo (getDual (foldMap (Dual . Endo . flip f) t)) z + default B foldl(BiFunction f, B z, TApp t) { + Dual> dualEndo = + foldMap(Dual.monoid(Endo.monoid()), curry(flip(f)).andThen(Endo::of).andThen(Dual::of), t); + return dualEndo.getDual().appEndo().apply(z); + } + + // toList :: t a -> [a] + default FwdList toList(TApp ta) { + return FwdList.build( + new FwdList.Builder<>() { + @Override + public B apply(BiFunction cons, Supplier nil) { + return foldr(cons, nil.get(), ta); + } + }); + } + + // null :: t a -> Bool + default boolean isEmpty(TApp ta) { + return foldr((a, b) -> false, true, ta); + } + + // length :: t a -> Int + default int length(TApp ta) { + return foldl((n, a) -> n + 1, 0, ta); + } +} + +record Endo(Function appEndo) { + public Endo compose(Endo other) { + return new Endo<>(a -> appEndo.apply(other.appEndo.apply(a))); + } + + public static Endo id() { + return new Endo<>(a -> a); + } + + public static Endo of(Function f) { + return new Endo<>(f); + } + + public static Function> of(BiFunction f) { + return a -> new Endo<>(b -> f.apply(a, b)); + } + + @TypeClass.Witness + static Monoid> monoid() { + return Monoid.of(Endo::id, Endo::compose); + } +} + +record Dual(A getDual) { + public static Dual of(A a) { + return new Dual<>(a); + } + + public static Monoid> monoid(Monoid monoidA) { + return Monoid.of( + () -> new Dual<>(monoidA.identity()), + (d1, d2) -> new Dual<>(monoidA.combine(d2.getDual, d1.getDual))); + } +} diff --git a/src/main/java/com/garciat/typeclasses/classes/Functor.java b/src/main/java/com/garciat/typeclasses/classes/Functor.java new file mode 100644 index 0000000..7730dc2 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/Functor.java @@ -0,0 +1,13 @@ +package com.garciat.typeclasses.classes; + +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 java.util.function.Function; + +@TypeClass +public interface Functor>> { + TApp map(Function f, TApp fa); +} diff --git a/src/main/java/com/garciat/typeclasses/classes/Monad.java b/src/main/java/com/garciat/typeclasses/classes/Monad.java new file mode 100644 index 0000000..d0e5916 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/Monad.java @@ -0,0 +1,23 @@ +package com.garciat.typeclasses.classes; + +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 java.util.function.Function; + +@TypeClass +public interface Monad>> extends Applicative { + TApp flatMap(Function> f, TApp fa); + + @Override + default TApp map(Function f, TApp fa) { + return flatMap(a -> pure(f.apply(a)), fa); + } + + @Override + default TApp ap(TApp> ff, TApp fa) { + return flatMap(a -> map(f -> f.apply(a), ff), fa); + } +} diff --git a/src/main/java/com/garciat/typeclasses/classes/Monoid.java b/src/main/java/com/garciat/typeclasses/classes/Monoid.java new file mode 100644 index 0000000..c31dfca --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/Monoid.java @@ -0,0 +1,50 @@ +package com.garciat.typeclasses.classes; + +import com.garciat.typeclasses.api.TypeClass; +import java.util.List; +import java.util.function.BinaryOperator; +import java.util.function.Supplier; + +@TypeClass +public interface Monoid { + A combine(A a1, A a2); + + A identity(); + + static A combineAll(Monoid monoid, List elements) { + A result = monoid.identity(); + for (A element : elements) { + result = monoid.combine(result, element); + } + return result; + } + + static Monoid of(Supplier identity, BinaryOperator combine) { + return new Monoid<>() { + @Override + public A combine(A a1, A a2) { + return combine.apply(a1, a2); + } + + @Override + public A identity() { + return identity.get(); + } + }; + } + + @TypeClass.Witness + static Monoid stringMonoid() { + return new Monoid<>() { + @Override + public String combine(String s1, String s2) { + return s1 + s2; + } + + @Override + public String identity() { + return ""; + } + }; + } +} diff --git a/src/main/java/com/garciat/typeclasses/classes/Num.java b/src/main/java/com/garciat/typeclasses/classes/Num.java new file mode 100644 index 0000000..3611edd --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/Num.java @@ -0,0 +1,39 @@ +package com.garciat.typeclasses.classes; + +import com.garciat.typeclasses.api.TypeClass; + +@TypeClass +public interface Num { + A add(A a1, A a2); + + A mul(A a1, A a2); + + A zero(); + + A one(); + + @TypeClass.Witness + static Num integerNum() { + return new Num<>() { + @Override + public Integer add(Integer a1, Integer a2) { + return a1 + a2; + } + + @Override + public Integer mul(Integer a1, Integer a2) { + return a1 * a2; + } + + @Override + public Integer zero() { + return 0; + } + + @Override + public Integer one() { + return 1; + } + }; + } +} diff --git a/src/main/java/com/garciat/typeclasses/classes/Ord.java b/src/main/java/com/garciat/typeclasses/classes/Ord.java new file mode 100644 index 0000000..308c9f0 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/Ord.java @@ -0,0 +1,42 @@ +package com.garciat.typeclasses.classes; + +import com.garciat.typeclasses.api.TypeClass; +import java.util.Optional; + +@TypeClass +public interface Ord extends Eq { + Ordering compare(A a1, A a2); + + @Override + default boolean eq(A a1, A a2) { + return compare(a1, a2) == Ordering.EQ; + } + + static Ordering compare(Ord ordA, A a1, A a2) { + return ordA.compare(a1, a2); + } + + static boolean lt(Ord ordA, A a1, A a2) { + return ordA.compare(a1, a2) == Ordering.LT; + } + + @TypeClass.Witness + static Ord integerOrd() { + return (a1, a2) -> a1 < a2 ? Ordering.LT : a1 > a2 ? Ordering.GT : Ordering.EQ; + } + + @TypeClass.Witness + static Ord> optionalOrd(Ord ordA) { + return (optA1, optA2) -> { + if (optA1.isPresent() && optA2.isPresent()) { + return ordA.compare(optA1.get(), optA2.get()); + } else if (optA1.isEmpty() && optA2.isEmpty()) { + return Ordering.EQ; + } else if (optA1.isEmpty()) { + return Ordering.LT; + } else { + return Ordering.GT; + } + }; + } +} 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/classes/PrintAll.java b/src/main/java/com/garciat/typeclasses/classes/PrintAll.java new file mode 100644 index 0000000..919be1f --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/PrintAll.java @@ -0,0 +1,53 @@ +package com.garciat.typeclasses.classes; + +import com.garciat.typeclasses.api.TypeClass; +import com.garciat.typeclasses.api.TypeClass.Witness; +import com.garciat.typeclasses.impl.utils.Lists; +import com.garciat.typeclasses.types.F1; +import com.garciat.typeclasses.types.F2; +import com.garciat.typeclasses.types.F3; +import java.util.List; +import java.util.function.Function; + +/** + * @implNote Source + */ +@TypeClass +public interface PrintAll { + T printAll(List strings); + + static T of(PrintAll printAll) { + return printAll.printAll(List.of()); + } + + @Witness + static PrintAll base() { + return strings -> { + for (String s : strings) { + System.out.println(s); + } + return null; + }; + } + + @Witness + static PrintAll> func(Show showA, PrintAll printR) { + return strings -> a -> printR.printAll(Lists.concat(strings, List.of(showA.show(a)))); + } + + @Witness + static PrintAll> func1(PrintAll> printR) { + return strings -> F1.of(printR.printAll(strings)); + } + + @Witness + static PrintAll> func2(PrintAll>> printR) { + return strings -> F2.of(printR.printAll(strings)); + } + + @Witness + static PrintAll> func3( + PrintAll>>> printR) { + return strings -> F3.of(printR.printAll(strings)); + } +} diff --git a/src/main/java/com/garciat/typeclasses/classes/Random.java b/src/main/java/com/garciat/typeclasses/classes/Random.java new file mode 100644 index 0000000..5faefc5 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/Random.java @@ -0,0 +1,19 @@ +package com.garciat.typeclasses.classes; + +import com.garciat.typeclasses.api.TypeClass; +import com.garciat.typeclasses.types.Pair; + +@TypeClass +public interface Random { + Pair random(RandomGen randomGen, G gen); + + @TypeClass.Witness + static Random integerRandom() { + return new Random<>() { + @Override + public Pair random(RandomGen randomGen, G gen) { + return randomGen.next(gen); + } + }; + } +} diff --git a/src/main/java/com/garciat/typeclasses/classes/RandomGen.java b/src/main/java/com/garciat/typeclasses/classes/RandomGen.java new file mode 100644 index 0000000..e167f1f --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/RandomGen.java @@ -0,0 +1,28 @@ +package com.garciat.typeclasses.classes; + +import com.garciat.typeclasses.api.TypeClass; +import com.garciat.typeclasses.types.Pair; + +@TypeClass +public interface RandomGen { + Pair next(G gen); + + Pair split(G gen); + + @TypeClass.Witness + static RandomGen javaUtilRandomGen() { + return new RandomGen<>() { + @Override + public Pair next(java.util.Random gen) { + return Pair.of(gen.nextInt(), gen); + } + + @Override + public Pair split(java.util.Random gen) { + java.util.Random gen1 = new java.util.Random(gen.nextLong()); + java.util.Random gen2 = new java.util.Random(gen.nextLong()); + return Pair.of(gen1, gen2); + } + }; + } +} diff --git a/src/main/java/com/garciat/typeclasses/classes/Show.java b/src/main/java/com/garciat/typeclasses/classes/Show.java new file mode 100644 index 0000000..2b8c665 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/Show.java @@ -0,0 +1,59 @@ +package com.garciat.typeclasses.classes; + +import com.garciat.typeclasses.api.TypeClass; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@TypeClass +public interface Show { + String show(A a); + + static String show(Show showA, A a) { + return showA.show(a); + } + + @TypeClass.Witness + static Show integerShow() { + return i -> Integer.toString(i); + } + + @TypeClass.Witness + static Show stringShow() { + return s -> "\"" + s + "\""; + } + + @TypeClass.Witness + static Show> optionalShow(Show showA) { + return optA -> optA.map(a -> "Some(" + showA.show(a) + ")").orElse("None"); + } + + @TypeClass.Witness + static Show arrayShow(Show showA) { + return arrayA -> + Arrays.stream(arrayA).map(showA::show).collect(Collectors.joining(", ", "[", "]")); + } + + @TypeClass.Witness + static Show intArrayShow() { + return arrayA -> + Arrays.stream(arrayA) + .mapToObj(Integer::toString) + .collect(Collectors.joining(", ", "[", "]")); + } + + @TypeClass.Witness + static Show> listShow(Show showA) { + return listA -> listA.stream().map(showA::show).collect(Collectors.joining(", ", "[", "]")); + } + + @TypeClass.Witness + static Show> mapShow(Show showK, Show showV) { + return mapKV -> + mapKV.entrySet().stream() + .map(entry -> showK.show(entry.getKey()) + ": " + showV.show(entry.getValue())) + .collect(Collectors.joining(", ", "{", "}")); + } +} diff --git a/src/main/java/com/garciat/typeclasses/classes/SumAllInt.java b/src/main/java/com/garciat/typeclasses/classes/SumAllInt.java new file mode 100644 index 0000000..e2fe2a6 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/SumAllInt.java @@ -0,0 +1,45 @@ +package com.garciat.typeclasses.classes; + +import com.garciat.typeclasses.api.TypeClass; +import com.garciat.typeclasses.api.TypeClass.Witness; +import com.garciat.typeclasses.impl.utils.Lists; +import com.garciat.typeclasses.types.F1; +import com.garciat.typeclasses.types.F2; +import com.garciat.typeclasses.types.F3; +import java.util.List; +import java.util.function.Function; + +@TypeClass +public interface SumAllInt { + A sum(List list); + + static T of(SumAllInt sumAllInt) { + return sumAllInt.sum(List.of()); + } + + @Witness + static SumAllInt base() { + return list -> list.stream().mapToInt(Integer::intValue).sum(); + } + + @Witness + static SumAllInt> func(SumAllInt sumR, TyEq eq) { + return list -> a -> sumR.sum(Lists.concat(list, List.of(eq.castL(a)))); + } + + @Witness + static SumAllInt> func1(SumAllInt> sumR) { + return list -> F1.of(sumR.sum(list)); + } + + @Witness + static SumAllInt> func2(SumAllInt>> sumR) { + return list -> F2.of(sumR.sum(list)); + } + + @Witness + static SumAllInt> func3( + SumAllInt>>> sumR) { + return list -> F3.of(sumR.sum(list)); + } +} diff --git a/src/main/java/com/garciat/typeclasses/classes/Traversable.java b/src/main/java/com/garciat/typeclasses/classes/Traversable.java new file mode 100644 index 0000000..66e91af --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/Traversable.java @@ -0,0 +1,23 @@ +package com.garciat.typeclasses.classes; + +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 java.util.function.Function; + +@TypeClass +public interface Traversable>> extends Functor, Foldable { + >, A, B> TApp> traverse( + Applicative applicative, Function> f, TApp ta); + + static >, T extends Kind>, A, B> + TApp> traverse( + Traversable traversable, + Applicative applicative, + TApp tA, + Function> f) { + return traversable.traverse(applicative, f, tA); + } +} diff --git a/src/main/java/com/garciat/typeclasses/classes/TyEq.java b/src/main/java/com/garciat/typeclasses/classes/TyEq.java new file mode 100644 index 0000000..a926f2a --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/classes/TyEq.java @@ -0,0 +1,32 @@ +package com.garciat.typeclasses.classes; + +import com.garciat.typeclasses.api.TypeClass; + +@TypeClass +public sealed interface TyEq { + A castR(B b); + + B castL(A a); + + static TyEq refl() { + return new Refl<>(); + } + + record Refl() implements TyEq { + + @Override + public T castR(T t) { + return t; + } + + @Override + public T castL(T t) { + return t; + } + } + + @TypeClass.Witness + static TyEq tyEqRefl() { + return refl(); + } +} diff --git a/src/main/java/com/garciat/typeclasses/impl/FuncType.java b/src/main/java/com/garciat/typeclasses/impl/FuncType.java new file mode 100644 index 0000000..b349e56 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/impl/FuncType.java @@ -0,0 +1,32 @@ +package com.garciat.typeclasses.impl; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.TypeVariable; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public record FuncType(Method java, List paramTypes, ParsedType returnType) { + public String format() { + return String.format( + "%s%s -> %s", + Arrays.stream(java.getTypeParameters()) + .map(TypeVariable::getName) + .reduce((a, b) -> a + " " + b) + .map("∀ %s. "::formatted) + .orElse(""), + paramTypes.stream().map(ParsedType::format).collect(Collectors.joining(", ", "(", ")")), + returnType.format()); + } + + public static FuncType parse(Method method) { + if (!Modifier.isStatic(method.getModifiers())) { + throw new IllegalArgumentException("Method must be static: " + method); + } + return new FuncType( + method, + ParsedType.parseAll(method.getGenericParameterTypes()), + ParsedType.parse(method.getGenericReturnType())); + } +} diff --git a/src/main/java/com/garciat/typeclasses/impl/ParsedType.java b/src/main/java/com/garciat/typeclasses/impl/ParsedType.java new file mode 100644 index 0000000..f87b622 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/impl/ParsedType.java @@ -0,0 +1,82 @@ +package com.garciat.typeclasses.impl; + +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.types.Maybe; +import com.garciat.typeclasses.types.Pair; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.List; + +public sealed interface ParsedType { + record Var(TypeVariable java) implements ParsedType {} + + record App(ParsedType fun, ParsedType arg) implements ParsedType {} + + record ArrayOf(ParsedType elementType) implements ParsedType {} + + record Const(Class java) implements ParsedType {} + + record Primitive(Class java) implements ParsedType {} + + default String format() { + return switch (this) { + case Var v -> v.java.getName(); + case Const c -> + c.java().getSimpleName() + + Arrays.stream(c.java().getTypeParameters()) + .map(TypeVariable::getName) + .reduce((a, b) -> a + ", " + b) + .map(s -> "[" + s + "]") + .orElse(""); + case App a -> a.fun.format() + "(" + a.arg.format() + ")"; + case ArrayOf a -> a.elementType.format() + "[]"; + case Primitive p -> p.java().getSimpleName(); + }; + } + + static List parseAll(Type[] types) { + return Arrays.stream(types).map(ParsedType::parse).toList(); + } + + static ParsedType parse(Type java) { + return switch (java) { + case Class tag when parseTagType(tag) instanceof Maybe.Just>(var tagged) -> + new Const(tagged); + case Class arr when arr.isArray() -> new ArrayOf(parse(arr.getComponentType())); + case Class prim when prim.isPrimitive() -> new Primitive(prim); + case Class c -> new Const(c); + case TypeVariable v -> new Var(v); + case ParameterizedType p + when parseAppType(p) + instanceof Maybe.Just>(Pair(var fun, var arg)) -> + new App(parse(fun), parse(arg)); + case ParameterizedType p -> + parseAll(p.getActualTypeArguments()).stream().reduce(parse(p.getRawType()), App::new); + case GenericArrayType a -> new ArrayOf(parse(a.getGenericComponentType())); + case WildcardType w -> throw new IllegalArgumentException("Cannot parse wildcard type: " + w); + default -> throw new IllegalArgumentException("Unsupported type: " + java); + }; + } + + private static Maybe> parseTagType(Class c) { + return switch (c.getEnclosingClass()) { + case Class enclosing when c.getSuperclass().equals(TagBase.class) -> Maybe.just(enclosing); + case null -> Maybe.nothing(); + default -> Maybe.nothing(); + }; + } + + private static Maybe> parseAppType(ParameterizedType t) { + return switch (t.getRawType()) { + case Class raw when raw.equals(TApp.class) || raw.equals(TPar.class) -> + Maybe.just(Pair.of(t.getActualTypeArguments()[0], t.getActualTypeArguments()[1])); + default -> Maybe.nothing(); + }; + } +} diff --git a/src/main/java/com/garciat/typeclasses/impl/Unification.java b/src/main/java/com/garciat/typeclasses/impl/Unification.java new file mode 100644 index 0000000..0b73712 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/impl/Unification.java @@ -0,0 +1,50 @@ +package com.garciat.typeclasses.impl; + +import com.garciat.typeclasses.impl.utils.Maps; +import com.garciat.typeclasses.types.Maybe; +import com.garciat.typeclasses.types.Pair; +import java.util.List; +import java.util.Map; + +public class Unification { + public static Maybe> unify(ParsedType t1, ParsedType t2) { + return switch (Pair.of(t1, t2)) { + case Pair(ParsedType.Var var1, ParsedType.Primitive p) -> + Maybe.nothing(); // no primitives in generics + case Pair(ParsedType.Var var1, var t) -> Maybe.just(Map.of(var1, t)); + case Pair(ParsedType.Const const1, ParsedType.Const const2) + when const1.equals(const2) -> + Maybe.just(Map.of()); + case Pair( + ParsedType.App(var fun1, var arg1), + ParsedType.App(var fun2, var arg2)) -> + Maybe.apply(Maps::merge, unify(fun1, fun2), unify(arg1, arg2)); + case Pair( + ParsedType.ArrayOf(var elem1), + ParsedType.ArrayOf(var elem2)) -> + unify(elem1, elem2); + case Pair( + ParsedType.Primitive(var prim1), + ParsedType.Primitive(var prim2)) + when prim1.equals(prim2) -> + Maybe.just(Map.of()); + default -> Maybe.nothing(); + }; + } + + public static ParsedType substitute(Map map, ParsedType type) { + return switch (type) { + case ParsedType.Var var -> map.getOrDefault(var, var); + case ParsedType.App(var fun, var arg) -> + new ParsedType.App(substitute(map, fun), substitute(map, arg)); + case ParsedType.ArrayOf var -> new ParsedType.ArrayOf(substitute(map, var.elementType())); + case ParsedType.Primitive p -> p; + case ParsedType.Const c -> c; + }; + } + + public static List substituteAll( + Map map, List types) { + return types.stream().map(t -> substitute(map, t)).toList(); + } +} diff --git a/src/main/java/com/garciat/typeclasses/impl/utils/Functions.java b/src/main/java/com/garciat/typeclasses/impl/utils/Functions.java new file mode 100644 index 0000000..5483577 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/impl/utils/Functions.java @@ -0,0 +1,16 @@ +package com.garciat.typeclasses.impl.utils; + +import java.util.function.BiFunction; +import java.util.function.Function; + +public final class Functions { + private 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/utils/Lists.java b/src/main/java/com/garciat/typeclasses/impl/utils/Lists.java new file mode 100644 index 0000000..d25373c --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/impl/utils/Lists.java @@ -0,0 +1,19 @@ +package com.garciat.typeclasses.impl.utils; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +public final class Lists { + private 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/utils/Maps.java b/src/main/java/com/garciat/typeclasses/impl/utils/Maps.java new file mode 100644 index 0000000..cea9e49 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/impl/utils/Maps.java @@ -0,0 +1,19 @@ +package com.garciat.typeclasses.impl.utils; + +import java.util.HashMap; +import java.util.Map; + +public final class Maps { + private 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/utils/ZeroOneMore.java b/src/main/java/com/garciat/typeclasses/impl/utils/ZeroOneMore.java new file mode 100644 index 0000000..bc51341 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/impl/utils/ZeroOneMore.java @@ -0,0 +1,19 @@ +package com.garciat.typeclasses.impl.utils; + +import java.util.List; + +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/com/garciat/typeclasses/types/Either.java b/src/main/java/com/garciat/typeclasses/types/Either.java new file mode 100644 index 0000000..2cba8a5 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/types/Either.java @@ -0,0 +1,88 @@ +package com.garciat.typeclasses.types; + +import com.garciat.typeclasses.api.TypeClass.Witness; +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.Applicative; +import com.garciat.typeclasses.classes.Functor; +import com.garciat.typeclasses.classes.Monad; +import java.util.List; +import java.util.function.Function; + +public sealed interface Either extends TApp, R> { + record Left(L value) implements Either {} + + record Right(R value) implements Either {} + + static Either left(L value) { + return new Left<>(value); + } + + static Either right(R value) { + return new Right<>(value); + } + + default Either map(Function f) { + return fold(Either::left, f.andThen(Either::right)); + } + + default Either mapLeft(Function f) { + return fold(f.andThen(Either::left), Either::right); + } + + default Either flatMap(Function> f) { + return fold(Either::left, f); + } + + default A fold( + Function fLeft, Function fRight) { + return switch (this) { + case Left(L value) -> fLeft.apply(value); + case Right(R value) -> fRight.apply(value); + }; + } + + static Either> traverse(List list, Function> f) { + return unwrap(JavaList.of(list).traverse(Either.applicative(), f::apply)).map(JavaList::toList); + } + + @Witness + static Functor> functor() { + return new Functor<>() { + @Override + public TApp, B> map(Function f, TApp, A> fa) { + return unwrap(fa).map(f); + } + }; + } + + @Witness + static Applicative> applicative() { + return monad(); + } + + @Witness + static Monad> monad() { + return new Monad<>() { + @Override + public TApp, A> pure(A a) { + return right(a); + } + + @Override + public TApp, B> flatMap( + Function, B>> f, TApp, A> fa) { + return unwrap(fa).flatMap(a -> unwrap(f.apply(a))); + } + }; + } + + final class Tag extends TagBase>> {} + + static Either unwrap(TApp, R> value) { + return (Either) value; + } +} diff --git a/src/main/java/com/garciat/typeclasses/types/F1.java b/src/main/java/com/garciat/typeclasses/types/F1.java new file mode 100644 index 0000000..fbf71f1 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/types/F1.java @@ -0,0 +1,12 @@ +package com.garciat.typeclasses.types; + +import java.util.function.Function; + +@FunctionalInterface +public interface F1 { + R apply(A a); + + static F1 of(Function f) { + return f::apply; + } +} diff --git a/src/main/java/com/garciat/typeclasses/types/F2.java b/src/main/java/com/garciat/typeclasses/types/F2.java new file mode 100644 index 0000000..b0b52c7 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/types/F2.java @@ -0,0 +1,12 @@ +package com.garciat.typeclasses.types; + +import java.util.function.Function; + +@FunctionalInterface +public interface F2 { + R apply(A a, B b); + + static F2 of(Function> f) { + return (a, b) -> f.apply(a).apply(b); + } +} diff --git a/src/main/java/com/garciat/typeclasses/types/F3.java b/src/main/java/com/garciat/typeclasses/types/F3.java new file mode 100644 index 0000000..ee38c59 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/types/F3.java @@ -0,0 +1,12 @@ +package com.garciat.typeclasses.types; + +import java.util.function.Function; + +@FunctionalInterface +public interface F3 { + R apply(A a, B b, C c); + + static F3 of(Function>> f) { + return (a, b, c) -> f.apply(a).apply(b).apply(c); + } +} diff --git a/src/main/java/com/garciat/typeclasses/types/FwdList.java b/src/main/java/com/garciat/typeclasses/types/FwdList.java new file mode 100644 index 0000000..ccd5c48 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/types/FwdList.java @@ -0,0 +1,209 @@ +package com.garciat.typeclasses.types; + +import static com.garciat.typeclasses.api.TypeClass.Witness.Overlap.OVERLAPPING; + +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.TagBase; +import com.garciat.typeclasses.classes.Applicative; +import com.garciat.typeclasses.classes.Foldable; +import com.garciat.typeclasses.classes.Functor; +import com.garciat.typeclasses.classes.Monad; +import com.garciat.typeclasses.classes.Monoid; +import com.garciat.typeclasses.classes.Show; +import com.garciat.typeclasses.classes.Traversable; +import com.garciat.typeclasses.classes.TyEq; +import java.util.Arrays; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public sealed interface FwdList extends TApp { + record Nil() implements FwdList {} + + record Cons(A head, FwdList tail) implements FwdList {} + + default R match(Supplier onNil, BiFunction, R> onCons) { + return switch (this) { + case Nil() -> onNil.get(); + case Cons(A head, FwdList tail) -> onCons.apply(head, tail); + }; + } + + default M foldMap(Monoid monoid, Function f) { + return match( + monoid::identity, (head, tail) -> monoid.combine(f.apply(head), tail.foldMap(monoid, f))); + } + + default B foldr(B identity, BiFunction f) { + return match(() -> identity, (head, tail) -> f.apply(head, tail.foldr(identity, f))); + } + + default B foldl(B identity, BiFunction f) { + return match(() -> identity, (head, tail) -> tail.foldl(f.apply(identity, head), f)); + } + + default void forEach(Consumer action) { + this.match( + () -> null, + (head, tail) -> { + action.accept(head); + tail.forEach(action); + return null; + }); + } + + default FwdList map(Function f) { + return match(FwdList::of, (head, tail) -> cons(f.apply(head), tail.map(f))); + } + + default FwdList flatMap(Function> f) { + return match(FwdList::of, (head, tail) -> append(f.apply(head), tail.flatMap(f))); + } + + default >, B> TApp> traverse( + Applicative applicative, Function> f) { + return foldr( + applicative.pure(FwdList.of()), + (head, tailT) -> + applicative.lift((B h, FwdList t) -> cons(h, t)).apply(f.apply(head), tailT)); + } + + static FwdList append(FwdList list1, FwdList list2) { + return list1.match(() -> list2, (head, tail) -> cons(head, append(tail, list2))); + } + + static FwdList of() { + return new Nil<>(); + } + + static FwdList cons(A head, FwdList tail) { + return new Cons<>(head, tail); + } + + static FwdList of(A a) { + return cons(a, of()); + } + + @SafeVarargs + static FwdList of(A... items) { + return of(Arrays.asList(items)); + } + + static FwdList of(Iterable iter) { + return unfoldr( + iter.iterator(), it -> it.hasNext() ? Maybe.just(Pair.of(it.next(), it)) : Maybe.nothing()); + } + + static FwdList of(CharSequence str) { + return unfoldr( + 0, + index -> + index < str.length() + ? Maybe.just(Pair.of(str.charAt(index), index + 1)) + : Maybe.nothing()); + } + + default String toStr(TyEq ty) { + StringBuilder sb = new StringBuilder(); + forEach(ch -> sb.append((char) ty.castL(ch))); + return sb.toString(); + } + + static FwdList unfoldr(B seed, Function>> f) { + Maybe> result = f.apply(seed); + return result.fold(FwdList::of, pair -> cons(pair.fst(), unfoldr(pair.snd(), f))); + } + + public static FwdList build(Builder builder) { + return builder.apply(FwdList::cons, FwdList::of); + } + + public interface Builder { + B apply(BiFunction cons, Supplier nil); + } + + @com.garciat.typeclasses.api.TypeClass.Witness + static Show> show(Show showA) { + return list -> { + StringBuilder sb = new StringBuilder(); + sb.append("["); + list.forEach(a -> sb.append(showA.show(a))); + sb.append("]"); + return sb.toString(); + }; + } + + @com.garciat.typeclasses.api.TypeClass.Witness(overlap = OVERLAPPING) + static Show> show() { + return list -> { + StringBuilder sb = new StringBuilder(); + sb.append("\""); + list.forEach(sb::append); + sb.append("\""); + return sb.toString(); + }; + } + + @com.garciat.typeclasses.api.TypeClass.Witness + static Functor functor() { + return new Control(); + } + + @com.garciat.typeclasses.api.TypeClass.Witness + static Foldable foldable() { + return new Control(); + } + + @com.garciat.typeclasses.api.TypeClass.Witness + static Traversable traversable() { + return new Control(); + } + + @com.garciat.typeclasses.api.TypeClass.Witness + static Applicative applicative() { + return new Control(); + } + + @com.garciat.typeclasses.api.TypeClass.Witness + static Monad monad() { + return new Control(); + } + + final class Control implements Traversable, Monad { + @Override + public TApp map(Function f, TApp fa) { + return unwrap(fa).map(f); + } + + @Override + public M foldMap(Monoid monoid, Function f, TApp ta) { + return unwrap(ta).foldMap(monoid, f); + } + + @Override + public >, A, B> TApp> traverse( + Applicative applicative, Function> f, TApp ta) { + return unwrap(ta).traverse(applicative, f); + } + + @Override + public TApp pure(A a) { + return FwdList.of(a); + } + + @Override + public TApp flatMap(Function> f, TApp fa) { + return unwrap(fa).flatMap(a -> unwrap(f.apply(a))); + } + } + + final class Tag extends TagBase> {} + + static FwdList unwrap(TApp value) { + return (FwdList) value; + } +} diff --git a/src/main/java/com/garciat/typeclasses/types/Gen.java b/src/main/java/com/garciat/typeclasses/types/Gen.java new file mode 100644 index 0000000..65aceca --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/types/Gen.java @@ -0,0 +1,47 @@ +package com.garciat.typeclasses.types; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +@FunctionalInterface +public interface Gen { + A generate(long seed, int size); + + default Gen map(Function f) { + return (seed, size) -> f.apply(generate(seed, size)); + } + + // TODO: This is a naive implementation; in a real implementation, the seed + // management would be + // more sophisticated. + default Gen flatMap(Function> f) { + return (seed, size) -> f.apply(generate(seed, size)).generate(seed + 1, size); + } + + default Gen variant(int n) { + return (seed, size) -> generate(seed + n, size); + } + + default Gen> listOf() { + return sized(size -> chooseInt(0, size).flatMap(this::vectorOf)); + } + + default Gen> vectorOf(int length) { + return (seed, size) -> { + List result = new ArrayList<>(); + for (int i = 0; i < length; i++) { + result.add(generate(seed + i, size)); + } + return result; + }; + } + + static Gen chooseInt(int low, int high) { + return (seed, size) -> new java.util.Random(seed).nextInt(low, high); + } + + static Gen sized(Function> gen) { + return (seed, size) -> gen.apply(size).generate(seed, size); + } +} diff --git a/src/main/java/com/garciat/typeclasses/types/JavaList.java b/src/main/java/com/garciat/typeclasses/types/JavaList.java new file mode 100644 index 0000000..234f32f --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/types/JavaList.java @@ -0,0 +1,102 @@ +package com.garciat.typeclasses.types; + +import com.garciat.typeclasses.api.TypeClass.Witness; +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.TagBase; +import com.garciat.typeclasses.classes.Applicative; +import com.garciat.typeclasses.classes.Functor; +import com.garciat.typeclasses.classes.Monoid; +import com.garciat.typeclasses.classes.Show; +import com.garciat.typeclasses.classes.Traversable; +import com.garciat.typeclasses.impl.utils.Lists; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +public record JavaList(List toList) implements TApp { + public JavaList map(Function f) { + return new JavaList<>(toList().stream().map(f).toList()); + } + + public JavaList flatMap(Function> f) { + List result = new ArrayList<>(); + for (A item : toList()) { + result.addAll(f.apply(item).toList()); + } + return new JavaList<>(result); + } + + public M foldMap(Monoid monoid, Function f) { + return toList().stream().map(f).reduce(monoid.identity(), monoid::combine); + } + + public >, B> TApp> traverse( + Applicative applicative, Function> f) { + TApp> result = applicative.pure(JavaList.of()); + for (A item : toList()) { + TApp fb = f.apply(item); + result = + applicative + .lift((JavaList bs, B b) -> JavaList.of(Lists.concat(bs.toList(), List.of(b)))) + .apply(result, fb); + } + return result; + } + + public static JavaList of() { + return new JavaList<>(List.of()); + } + + @SafeVarargs + public static JavaList of(T... items) { + return new JavaList<>(List.of(items)); + } + + public static JavaList of(List list) { + return new JavaList<>(list); + } + + @Witness + public static Show> show(Show showA) { + return listA -> + listA.toList().stream().map(showA::show).collect(Collectors.joining(", ", "[", "]")); + } + + @Witness + public static Functor functor() { + return new Control(); + } + + @Witness + public static Traversable traversable() { + return new Control(); + } + + private static final class Control implements Traversable { + @Override + public TApp map(Function f, TApp fa) { + return unwrap(fa).map(f); + } + + @Override + public M foldMap(Monoid monoid, Function f, TApp ta) { + return unwrap(ta).foldMap(monoid, f); + } + + @Override + public >, A, B> TApp> traverse( + Applicative applicative, Function> f, TApp ta) { + return unwrap(ta).traverse(applicative, f); + } + } + + public static JavaList unwrap(TApp x) { + return (JavaList) x; + } + + public static final class Tag extends TagBase> {} +} diff --git a/src/main/java/com/garciat/typeclasses/types/Maybe.java b/src/main/java/com/garciat/typeclasses/types/Maybe.java new file mode 100644 index 0000000..30a00ba --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/types/Maybe.java @@ -0,0 +1,108 @@ +package com.garciat.typeclasses.types; + +import com.garciat.typeclasses.api.TypeClass.Witness; +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.TagBase; +import com.garciat.typeclasses.classes.Applicative; +import com.garciat.typeclasses.classes.Functor; +import com.garciat.typeclasses.classes.Monad; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public sealed interface Maybe extends TApp { + record Just(A value) implements Maybe {} + + record Nothing() implements Maybe {} + + static Maybe just(A value) { + return new Just<>(value); + } + + static Maybe nothing() { + return new Nothing<>(); + } + + default R fold(Supplier onNothing, Function onJust) { + return switch (this) { + case Just(A value) -> onJust.apply(value); + case Nothing() -> onNothing.get(); + }; + } + + default Maybe filter(Function predicate) { + return flatMap(a -> predicate.apply(a) ? just(a) : nothing()); + } + + default Stream stream() { + return fold(Stream::empty, Stream::of); + } + + default Maybe map(Function f) { + return fold(Maybe::nothing, a -> just(f.apply(a))); + } + + default Maybe flatMap(Function> f) { + return switch (this) { + case Just(A value) -> f.apply(value); + case Nothing() -> nothing(); + }; + } + + static BiFunction, Maybe, Maybe> lift(BiFunction f) { + return (ma, mb) -> ma.flatMap(a -> mb.map(b -> f.apply(a, b))); + } + + static Maybe apply(BiFunction f, Maybe ma, Maybe mb) { + return lift(f).apply(ma, mb); + } + + @Witness + static Functor functor() { + return new Functor<>() { + @Override + public TApp map(Function f, TApp fa) { + return unwrap(fa).map(f); + } + }; + } + + @Witness + static Applicative applicative() { + return new Applicative<>() { + @Override + public TApp pure(A a) { + return just(a); + } + + @Override + public TApp ap(TApp> ff, TApp fa) { + return unwrap(ff).flatMap(f -> unwrap(fa).map(f)); + } + }; + } + + @Witness + static Monad monad() { + return new Monad<>() { + @Override + public TApp pure(A a) { + return just(a); + } + + @Override + public TApp flatMap(Function> f, TApp fa) { + return unwrap(fa).flatMap(a -> unwrap(f.apply(a))); + } + }; + } + + final class Tag extends TagBase> {} + + static Maybe unwrap(TApp value) { + return (Maybe) value; + } +} diff --git a/src/main/java/com/garciat/typeclasses/types/Pair.java b/src/main/java/com/garciat/typeclasses/types/Pair.java new file mode 100644 index 0000000..c17e757 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/types/Pair.java @@ -0,0 +1,18 @@ +package com.garciat.typeclasses.types; + +import java.util.function.Function; + +public record Pair(A fst, B snd) { + + public Pair mapFst(Function f) { + return Pair.of(f.apply(fst), snd); + } + + public Pair mapSnd(Function f) { + return Pair.of(fst, f.apply(snd)); + } + + public static Pair of(A first, B second) { + return new Pair<>(first, second); + } +} diff --git a/src/main/java/com/garciat/typeclasses/types/Parser.java b/src/main/java/com/garciat/typeclasses/types/Parser.java new file mode 100644 index 0000000..325d2e8 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/types/Parser.java @@ -0,0 +1,117 @@ +package com.garciat.typeclasses.types; + +import static com.garciat.typeclasses.classes.TyEq.refl; + +import com.garciat.typeclasses.api.TypeClass.Witness; +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.TagBase; +import com.garciat.typeclasses.classes.Alternative; +import com.garciat.typeclasses.classes.Applicative; +import com.garciat.typeclasses.classes.Functor; +import com.garciat.typeclasses.classes.Monad; +import java.util.function.Function; +import java.util.function.Predicate; + +@FunctionalInterface +public interface Parser extends TApp { + Maybe>> parse(FwdList input); + + default Parser map(Function f) { + return input -> parse(input).map(pair -> pair.mapFst(f)); + } + + default Parser flatMap(Function> f) { + return input -> parse(input).flatMap(pair -> f.apply(pair.fst()).parse(pair.snd())); + } + + default Parser or(Parser other) { + return input -> parse(input).fold(() -> other.parse(input), Maybe::just); + } + + default Parser applyTo(Parser> pf) { + return pf.flatMap(this::map); + } + + static Parser pure(A a) { + return input -> Maybe.just(Pair.of(a, input)); + } + + static Parser fail() { + return input -> Maybe.nothing(); + } + + static Parser satisfy(Predicate predicate) { + return input -> + input.match( + () -> Maybe.nothing(), + (head, tail) -> + predicate.test(head) ? Maybe.just(Pair.of(head, tail)) : Maybe.nothing()); + } + + static Parser charParser(char c) { + return satisfy(ch -> ch == c); + } + + static Parser stringParser(String str) { + return unwrap(FwdList.of(str).traverse(applicative(), Parser::charParser)) + .map(cs -> cs.toStr(refl())); + } + + @Witness + static Functor functor() { + return Control.INSTANCE; + } + + @Witness + static Applicative applicative() { + return Control.INSTANCE; + } + + @Witness + static Alternative alternative() { + return Control.INSTANCE; + } + + @Witness + static Monad monad() { + return Control.INSTANCE; + } + + final class Control implements Monad, Alternative { + private static final Control INSTANCE = new Control(); + + @Override + public TApp pure(A a) { + return Parser.pure(a); + } + + @Override + public TApp ap(TApp> ff, TApp fa) { + return unwrap(fa).applyTo(unwrap(ff)); + } + + @Override + public TApp flatMap( + Function> f, TApp fa) { + return unwrap(fa).flatMap(a -> unwrap(f.apply(a))); + } + + @Override + public TApp empty() { + return Parser.fail(); + } + + @Override + public TApp alt(TApp fa1, TApp fa2) { + return unwrap(fa1).or(unwrap(fa2)); + } + } + + final class Tag extends TagBase> {} + + static Parser unwrap(TApp value) { + return (Parser) value; + } +} diff --git a/src/main/java/com/garciat/typeclasses/types/State.java b/src/main/java/com/garciat/typeclasses/types/State.java new file mode 100644 index 0000000..ac55397 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/types/State.java @@ -0,0 +1,84 @@ +package com.garciat.typeclasses.types; + +import com.garciat.typeclasses.api.TypeClass.Witness; +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.Applicative; +import com.garciat.typeclasses.classes.Functor; +import com.garciat.typeclasses.classes.Monad; +import java.util.function.Function; + +@FunctionalInterface +public interface State extends TApp, A> { + Pair run(S state); + + static State of(Function> f) { + return f::apply; + } + + static State pure(A a) { + return state -> Pair.of(a, state); + } + + default State map(Function f) { + return state -> run(state).mapFst(f); + } + + default State flatMap(Function> f) { + return state -> + switch (run(state)) { + case Pair(A a, S newState) -> f.apply(a).run(newState); + }; + } + + @Witness + static Functor> functor() { + return new Functor<>() { + @Override + public TApp, B> map(Function f, TApp, A> fa) { + return unwrap(fa).map(f); + } + }; + } + + @Witness + static Applicative> applicative() { + return new Applicative<>() { + @Override + public TApp, A> pure(A a) { + return State.pure(a); + } + + @Override + public TApp, B> ap( + TApp, Function> ff, TApp, A> fa) { + return unwrap(ff).flatMap(f -> unwrap(fa).map(f)); + } + }; + } + + @Witness + static Monad> monad() { + return new Monad<>() { + @Override + public TApp, A> pure(A a) { + return State.pure(a); + } + + @Override + public TApp, B> flatMap( + Function, B>> f, TApp, A> fa) { + return unwrap(fa).flatMap(a -> unwrap(f.apply(a))); + } + }; + } + + final class Tag extends TagBase>> {} + + static State unwrap(TApp, A> value) { + return (State) value; + } +} diff --git a/src/main/java/com/garciat/typeclasses/types/Sum.java b/src/main/java/com/garciat/typeclasses/types/Sum.java new file mode 100644 index 0000000..c018469 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/types/Sum.java @@ -0,0 +1,22 @@ +package com.garciat.typeclasses.types; + +import com.garciat.typeclasses.api.TypeClass; +import com.garciat.typeclasses.classes.Monoid; +import com.garciat.typeclasses.classes.Num; + +public record Sum(A value) { + @TypeClass.Witness + public static Monoid> monoid(Num num) { + return new Monoid<>() { + @Override + public Sum combine(Sum s1, Sum s2) { + return new Sum<>(num.add(s1.value(), s2.value())); + } + + @Override + public Sum identity() { + return new Sum<>(num.zero()); + } + }; + } +} 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..706de4e --- /dev/null +++ b/src/test/java/com/garciat/typeclasses/ExamplesTest.java @@ -0,0 +1,86 @@ +package com.garciat.typeclasses; + +import static com.garciat.typeclasses.TypeClasses.witness; + +import com.garciat.typeclasses.api.Ctx; +import com.garciat.typeclasses.api.Ty; +import com.garciat.typeclasses.classes.Arbitrary; +import com.garciat.typeclasses.classes.Eq; +import com.garciat.typeclasses.classes.Foldable; +import com.garciat.typeclasses.classes.Monoid; +import com.garciat.typeclasses.classes.Ord; +import com.garciat.typeclasses.classes.PrintAll; +import com.garciat.typeclasses.classes.Show; +import com.garciat.typeclasses.classes.SumAllInt; +import com.garciat.typeclasses.classes.Traversable; +import com.garciat.typeclasses.types.F3; +import com.garciat.typeclasses.types.FwdList; +import com.garciat.typeclasses.types.JavaList; +import com.garciat.typeclasses.types.Maybe; +import com.garciat.typeclasses.types.Sum; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import org.junit.jupiter.api.Test; + +final class ExamplesTest { + @Test + void example() { + 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))); + } + + private static void example(Show showA, A value) { + System.out.println(Show.show(witness(new Ty<>() {}, new Ctx<>(showA) {}), JavaList.of(value))); + } +} diff --git a/src/test/java/com/garciat/typeclasses/MainTest.java b/src/test/java/com/garciat/typeclasses/MainTest.java deleted file mode 100644 index 252d070..0000000 --- a/src/test/java/com/garciat/typeclasses/MainTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.garciat.typeclasses; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -final class MainTest { - @Test - void example() { - assertTrue(true); - } -}