diff --git a/pom.xml b/pom.xml index 0f66a65..6aed270 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,12 @@ 6.0.0 test + + org.assertj + assertj-core + 3.27.3 + test + diff --git a/src/test/java/com/garciat/typeclasses/TypeClassesTest.java b/src/test/java/com/garciat/typeclasses/TypeClassesTest.java new file mode 100644 index 0000000..5532d67 --- /dev/null +++ b/src/test/java/com/garciat/typeclasses/TypeClassesTest.java @@ -0,0 +1,231 @@ +package com.garciat.typeclasses; + +import static com.garciat.typeclasses.TypeClasses.witness; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.garciat.typeclasses.api.Ctx; +import com.garciat.typeclasses.api.Ty; +import com.garciat.typeclasses.testclasses.TestEq; +import com.garciat.typeclasses.testclasses.TestShow; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Test; + +final class TypeClassesTest { + + // ============================================ + // Basic witness resolution tests + // ============================================ + + @Test + void witnessSimpleTypeClass() { + TestShow showString = witness(new Ty<>() {}); + assertThat(showString).isNotNull(); + assertThat(showString.show("test")).isEqualTo("string:test"); + } + + @Test + void witnessWithDependency() { + TestShow> showOptional = witness(new Ty<>() {}); + assertThat(showOptional).isNotNull(); + assertThat(showOptional.show(Optional.of("test"))).isEqualTo("opt(string:test)"); + assertThat(showOptional.show(Optional.empty())).isEqualTo("empty"); + } + + @Test + void witnessWithMultipleDependencies() { + TestEq> eqList = witness(new Ty<>() {}); + assertThat(eqList).isNotNull(); + assertThat(eqList.eq(List.of("a", "b"), List.of("a", "b"))).isTrue(); + assertThat(eqList.eq(List.of("a", "b"), List.of("a", "c"))).isFalse(); + } + + // ============================================ + // Witness constructor lookup tests + // ============================================ + + @Test + void witnessLookupsInTypeClass() { + // Lookup should find witnesses in the type class interface (TestShow) + TestShow show = witness(new Ty<>() {}); + assertThat(show).isNotNull(); + } + + @Test + void witnessLookupsInTypeArguments() { + // Lookup should find witnesses in type arguments (String has witness in TestShow) + TestShow> show = witness(new Ty<>() {}); + assertThat(show).isNotNull(); + } + + // ============================================ + // Public static @TypeClass.Witness annotation tests + // ============================================ + + @Test + void witnessRequiresPublicStaticMethod() { + // This should work - witness is public static + TestShow show = witness(new Ty<>() {}); + assertThat(show).isNotNull(); + } + + @Test + void witnessNotFoundForUnannotatedTypes() { + // NoWitnessType has no @TypeClass.Witness methods + assertThatThrownBy(() -> witness(new Ty>() {})) + .isInstanceOf(TypeClasses.WitnessResolutionException.class); + } + + // ============================================ + // Recursive dependency resolution tests + // ============================================ + + @Test + void witnessRecursiveDependencies() { + // List> requires: + // 1. listShow(TestShow>) + // 2. optionalShow(TestShow) + // 3. stringShow() + TestShow>> show = witness(new Ty<>() {}); + assertThat(show).isNotNull(); + assertThat(show.show(List.of(Optional.of("a"), Optional.empty(), Optional.of("b")))) + .isEqualTo("[opt(string:a),empty,opt(string:b)]"); + } + + @Test + void witnessDeepRecursion() { + // List>> should resolve recursively + TestShow>>> show = witness(new Ty<>() {}); + assertThat(show).isNotNull(); + } + + // ============================================ + // Overlapping instances tests + // ============================================ + + @Test + void overlappingInstancesMoreSpecificWins() { + // When we have both general and specific instances, + // the more specific (overlapping) one should win + com.garciat.typeclasses.testclasses.OverlapShow show = witness(new Ty<>() {}); + assertThat(show).isNotNull(); + // Should use the specific Integer instance + assertThat(show.show(42)).isEqualTo("Integer: 42"); + } + + @Test + void overlappableInstanceCanBeOverridden() { + // OVERLAPPABLE instances can be overridden by more specific ones + com.garciat.typeclasses.testclasses.OverlapShow show = witness(new Ty<>() {}); + assertThat(show).isNotNull(); + assertThat(show.show("test")).isEqualTo("String: test"); + } + + // ============================================ + // Ambiguity detection tests + // ============================================ + + @Test + void ambiguousWitnessesThrow() { + // AmbiguousShow has two witness constructors without overlap markers + assertThatThrownBy( + () -> witness(new Ty>() {})) + .isInstanceOf(TypeClasses.WitnessResolutionException.class); + } + + // ============================================ + // Not found error tests + // ============================================ + + @Test + void witnessNotFoundThrows() { + assertThatThrownBy(() -> witness(new Ty>() {})) + .isInstanceOf(TypeClasses.WitnessResolutionException.class); + } + + @Test + void witnessNotFoundNestedThrows() { + // List - the dependency TestShow cannot be found + assertThatThrownBy(() -> witness(new Ty>>() {})) + .isInstanceOf(TypeClasses.WitnessResolutionException.class); + } + + // ============================================ + // Context/witness summoning tests + // ============================================ + + @Test + void witnessSummoningWithContext() { + // Provide a custom witness via context + CustomType customValue = new CustomType("test"); + TestShow customShow = c -> "custom:" + c.value; + + TestShow> listShow = witness(new Ty<>() {}, new Ctx<>(customShow) {}); + + assertThat(listShow).isNotNull(); + assertThat(listShow.show(List.of(new CustomType("a"), new CustomType("b")))) + .isEqualTo("[custom:a,custom:b]"); + } + + @Test + void witnessSummoningBuildsTree() { + // Verify that the witness is actually constructed correctly + // by checking its behavior with nested types + TestShow>> show = witness(new Ty<>() {}); + + assertThat(show.show(Optional.of(List.of("a", "b", "c")))) + .isEqualTo("opt([string:a,string:b,string:c])"); + assertThat(show.show(Optional.empty())).isEqualTo("empty"); + } + + // ============================================ + // WitnessResolutionException tests + // ============================================ + + @Test + void witnessResolutionExceptionHasMessage() { + assertThatThrownBy(() -> witness(new Ty>() {})) + .isInstanceOf(TypeClasses.WitnessResolutionException.class) + .hasMessageContaining("NoWitnessType"); + } + + @Test + void witnessResolutionExceptionForAmbiguous() { + assertThatThrownBy( + () -> witness(new Ty>() {})) + .isInstanceOf(TypeClasses.WitnessResolutionException.class) + .hasMessageContaining("mbiguous"); + } + + @Test + void witnessMapWithDependencies() { + // Map requires TestEq and TestEq + TestEq> eqMap = witness(new Ty<>() {}); + assertThat(eqMap).isNotNull(); + + Map map1 = Map.of("a", 1, "b", 2); + Map map2 = Map.of("a", 1, "b", 2); + Map map3 = Map.of("a", 1, "b", 3); + + assertThat(eqMap.eq(map1, map2)).isTrue(); + assertThat(eqMap.eq(map1, map3)).isFalse(); + } + + // ============================================ + // Test helper classes + // ============================================ + + static class NoWitnessType { + String value; + } + + static class CustomType { + String value; + + CustomType(String value) { + this.value = value; + } + } +} diff --git a/src/test/java/com/garciat/typeclasses/impl/FuncTypeTest.java b/src/test/java/com/garciat/typeclasses/impl/FuncTypeTest.java new file mode 100644 index 0000000..0fcd868 --- /dev/null +++ b/src/test/java/com/garciat/typeclasses/impl/FuncTypeTest.java @@ -0,0 +1,114 @@ +package com.garciat.typeclasses.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.garciat.typeclasses.impl.ParsedType.App; +import com.garciat.typeclasses.impl.ParsedType.Const; +import com.garciat.typeclasses.impl.ParsedType.Primitive; +import com.garciat.typeclasses.impl.ParsedType.Var; +import java.lang.reflect.Method; +import java.util.List; +import org.junit.jupiter.api.Test; + +final class FuncTypeTest { + @Test + void parseSimpleStaticMethod() throws Exception { + Method method = TestMethods.class.getDeclaredMethod("simple"); + + assertThat(FuncType.parse(method)) + .isEqualTo(new FuncType(method, List.of(), new Const(String.class))); + } + + @Test + void parseMethodWithParameters() throws Exception { + Method method = TestMethods.class.getDeclaredMethod("withParams", Integer.class, String.class); + + assertThat(FuncType.parse(method)) + .isEqualTo( + new FuncType( + method, + List.of(new Const(Integer.class), new Const(String.class)), + new Const(Boolean.class))); + } + + @Test + void parseMethodWithGenericReturn() throws Exception { + Method method = TestMethods.class.getDeclaredMethod("genericReturn"); + + assertThat(FuncType.parse(method)) + .isEqualTo( + new FuncType( + method, List.of(), new App(new Const(List.class), new Const(String.class)))); + } + + @Test + void parseMethodWithGenericParams() throws Exception { + Method method = TestMethods.class.getDeclaredMethod("genericParams", List.class); + + assertThat(FuncType.parse(method)) + .isEqualTo( + new FuncType( + method, + List.of(new App(new Const(List.class), new Const(Integer.class))), + new Primitive(void.class))); + } + + @Test + void parseMethodWithPrimitives() throws Exception { + Method method = TestMethods.class.getDeclaredMethod("withPrimitives", int.class, boolean.class); + + assertThat(FuncType.parse(method)) + .isEqualTo( + new FuncType( + method, + List.of(new Primitive(int.class), new Primitive(boolean.class)), + new Primitive(void.class))); + } + + @Test + void parseNonStaticMethodThrows() throws Exception { + Method method = TestMethods.class.getDeclaredMethod("nonStatic"); + assertThatThrownBy(() -> FuncType.parse(method)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void parseGenericMethodWithTypeParameters() throws Exception { + Method method = TestMethods.class.getDeclaredMethod("genericMethod", Object.class); + + // The method has a type parameter T, so we expect a Var in the return type + assertThat(FuncType.parse(method)) + .isEqualTo( + new FuncType( + method, + List.of(new Var(method.getTypeParameters()[0])), + new Var(method.getTypeParameters()[0]))); + } + + // Test helper class with various method signatures + private static class TestMethods { + public static String simple() { + return ""; + } + + public static Boolean withParams(Integer i, String s) { + return true; + } + + public static List genericReturn() { + return List.of(); + } + + public static void genericParams(List list) {} + + public static void withPrimitives(int i, boolean b) {} + + public static T genericMethod(T value) { + return value; + } + + public String nonStatic() { + return ""; + } + } +} diff --git a/src/test/java/com/garciat/typeclasses/impl/ParsedTypeTest.java b/src/test/java/com/garciat/typeclasses/impl/ParsedTypeTest.java new file mode 100644 index 0000000..66f12fe --- /dev/null +++ b/src/test/java/com/garciat/typeclasses/impl/ParsedTypeTest.java @@ -0,0 +1,100 @@ +package com.garciat.typeclasses.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.garciat.typeclasses.api.Ty; +import com.garciat.typeclasses.impl.ParsedType.App; +import com.garciat.typeclasses.impl.ParsedType.ArrayOf; +import com.garciat.typeclasses.impl.ParsedType.Const; +import com.garciat.typeclasses.impl.ParsedType.Primitive; +import com.garciat.typeclasses.impl.ParsedType.Var; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Test; + +final class ParsedTypeTest { + @Test + void parseClass() { + assertThat(ParsedType.parse(String.class)).isEqualTo(new Const(String.class)); + } + + @Test + void parsePrimitiveType() { + assertThat(ParsedType.parse(int.class)).isEqualTo(new Primitive(int.class)); + } + + @Test + void parseArrayType() { + assertThat(ParsedType.parse(int[].class)).isEqualTo(new ArrayOf(new Primitive(int.class))); + } + + @Test + void parseObjectArrayType() { + assertThat(ParsedType.parse(String[].class)).isEqualTo(new ArrayOf(new Const(String.class))); + } + + @Test + void parseParameterizedType() throws Exception { + assertThat(ParsedType.parse(new Ty>() {}.type())) + .isEqualTo(new App(new Const(List.class), new Const(String.class))); + } + + @Test + void parseMultipleTypeParameters() throws Exception { + assertThat(ParsedType.parse(new Ty>() {}.type())) + .isEqualTo( + new App( + new App(new Const(Map.class), new Const(String.class)), new Const(Integer.class))); + } + + @Test + void parseNestedParameterizedType() throws Exception { + assertThat(ParsedType.parse(new Ty>>() {}.type())) + .isEqualTo( + new App( + new Const(List.class), + new App(new Const(Optional.class), new Const(String.class)))); + } + + @Test + void parseAll() throws Exception { + assertThat(ParsedType.parseAll(new Type[] {String.class, Integer.class, int.class})) + .isEqualTo( + List.of(new Const(String.class), new Const(Integer.class), new Primitive(int.class))); + } + + @Test + void parseTypeVariable() throws Exception { + class TestClass {} + TypeVariable tv = TestClass.class.getTypeParameters()[0]; + + assertThat(ParsedType.parse(tv)).isEqualTo(new Var(tv)); + } + + @Test + void parseTypeVariableInParameterizedType() throws Exception { + class TestClass { + List field; + } + TypeVariable tv = TestClass.class.getTypeParameters()[0]; + + assertThat(ParsedType.parse(TestClass.class.getDeclaredField("field").getGenericType())) + .isEqualTo(new App(new Const(List.class), new Var(tv))); + } + + @Test + void parseWildcardTypeThrows() throws Exception { + class TestClass { + List field; + } + + assertThatThrownBy( + () -> ParsedType.parse(TestClass.class.getDeclaredField("field").getGenericType())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("wildcard"); + } +} diff --git a/src/test/java/com/garciat/typeclasses/impl/UnificationTest.java b/src/test/java/com/garciat/typeclasses/impl/UnificationTest.java new file mode 100644 index 0000000..f83fa48 --- /dev/null +++ b/src/test/java/com/garciat/typeclasses/impl/UnificationTest.java @@ -0,0 +1,201 @@ +package com.garciat.typeclasses.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.garciat.typeclasses.impl.ParsedType.App; +import com.garciat.typeclasses.impl.ParsedType.ArrayOf; +import com.garciat.typeclasses.impl.ParsedType.Const; +import com.garciat.typeclasses.impl.ParsedType.Primitive; +import com.garciat.typeclasses.impl.ParsedType.Var; +import com.garciat.typeclasses.types.Maybe; +import java.lang.reflect.TypeVariable; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +final class UnificationTest { + @Test + void unifyEqualConsts() { + ParsedType t1 = new Const(String.class); + ParsedType t2 = new Const(String.class); + + Maybe> result = Unification.unify(t1, t2); + + assertThat(result).isEqualTo(Maybe.just(Map.of())); + } + + @Test + void unifyDifferentConsts() { + ParsedType t1 = new Const(String.class); + ParsedType t2 = new Const(Integer.class); + + Maybe> result = Unification.unify(t1, t2); + + assertThat(result).isEqualTo(Maybe.nothing()); + } + + @Test + void unifyVarWithConst() throws Exception { + TypeVariable tv = getTypeVariable(); + ParsedType t1 = new Var(tv); + ParsedType t2 = new Const(String.class); + + Maybe> result = Unification.unify(t1, t2); + + assertThat(result).isEqualTo(Maybe.just(Map.of(t1, t2))); + } + + @Test + void unifyVarWithPrimitiveFails() throws Exception { + TypeVariable tv = getTypeVariable(); + ParsedType t1 = new Var(tv); + ParsedType t2 = new Primitive(int.class); + + Maybe> result = Unification.unify(t1, t2); + + assertThat(result).isEqualTo(Maybe.nothing()); + } + + @Test + void unifyApps() { + ParsedType list1 = new App(new Const(List.class), new Const(String.class)); + ParsedType list2 = new App(new Const(List.class), new Const(String.class)); + + Maybe> result = Unification.unify(list1, list2); + + assertThat(result).isEqualTo(Maybe.just(Map.of())); + } + + @Test + void unifyAppsDifferentArgs() { + ParsedType list1 = new App(new Const(List.class), new Const(String.class)); + ParsedType list2 = new App(new Const(List.class), new Const(Integer.class)); + + Maybe> result = Unification.unify(list1, list2); + + assertThat(result).isEqualTo(Maybe.nothing()); + } + + @Test + void unifyAppsWithVar() throws Exception { + TypeVariable tv = getTypeVariable(); + Var var = new Var(tv); + ParsedType list1 = new App(new Const(List.class), var); + ParsedType list2 = new App(new Const(List.class), new Const(String.class)); + + Maybe> result = Unification.unify(list1, list2); + + assertThat(result).isEqualTo(Maybe.just(Map.of(var, new Const(String.class)))); + } + + @Test + void unifyNestedAppsWithVar() throws Exception { + TypeVariable tv = getTypeVariable(); + Var var = new Var(tv); + ParsedType map1 = new App(new App(new Const(Map.class), var), new Const(Integer.class)); + ParsedType map2 = + new App(new App(new Const(Map.class), new Const(String.class)), new Const(Integer.class)); + + Maybe> result = Unification.unify(map1, map2); + + assertThat(result).isEqualTo(Maybe.just(Map.of(var, new Const(String.class)))); + } + + @Test + void unifyArrays() { + ParsedType arr1 = new ArrayOf(new Const(String.class)); + ParsedType arr2 = new ArrayOf(new Const(String.class)); + + Maybe> result = Unification.unify(arr1, arr2); + + assertThat(result).isEqualTo(Maybe.just(Map.of())); + } + + @Test + void unifyPrimitives() { + ParsedType p1 = new Primitive(int.class); + ParsedType p2 = new Primitive(int.class); + + Maybe> result = Unification.unify(p1, p2); + + assertThat(result).isEqualTo(Maybe.just(Map.of())); + } + + @Test + void unifyDifferentPrimitives() { + ParsedType p1 = new Primitive(int.class); + ParsedType p2 = new Primitive(boolean.class); + + Maybe> result = Unification.unify(p1, p2); + + assertThat(result).isEqualTo(Maybe.nothing()); + } + + @Test + void substituteVar() throws Exception { + TypeVariable tv = getTypeVariable(); + Var var = new Var(tv); + ParsedType replacement = new Const(String.class); + + ParsedType result = Unification.substitute(Map.of(var, replacement), var); + + assertThat(result).isEqualTo(replacement); + } + + @Test + void substituteConst() { + ParsedType type = new Const(String.class); + + ParsedType result = Unification.substitute(Map.of(), type); + + assertThat(result).isEqualTo(type); + } + + @Test + void substituteApp() throws Exception { + TypeVariable tv = getTypeVariable(); + Var var = new Var(tv); + ParsedType replacement = new Const(String.class); + + ParsedType result = + Unification.substitute(Map.of(var, replacement), new App(new Const(List.class), var)); + + assertThat(result).isEqualTo(new App(new Const(List.class), replacement)); + } + + @Test + void substituteArrayOf() throws Exception { + TypeVariable tv = getTypeVariable(); + Var var = new Var(tv); + ParsedType replacement = new Const(String.class); + + ParsedType result = Unification.substitute(Map.of(var, replacement), new ArrayOf(var)); + + assertThat(result).isEqualTo(new ArrayOf(replacement)); + } + + @Test + void substituteAll() throws Exception { + TypeVariable tv = getTypeVariable(); + Var var = new Var(tv); + ParsedType replacement = new Const(String.class); + + List result = + Unification.substituteAll( + Map.of(var, replacement), + List.of(var, new Const(Integer.class), new App(new Const(List.class), var))); + + assertThat(result) + .isEqualTo( + List.of( + replacement, + new Const(Integer.class), + new App(new Const(List.class), replacement))); + } + + // Helper to get a type variable for testing + private TypeVariable getTypeVariable() throws Exception { + class TestClass {} + return TestClass.class.getTypeParameters()[0]; + } +} diff --git a/src/test/java/com/garciat/typeclasses/testclasses/AmbiguousShow.java b/src/test/java/com/garciat/typeclasses/testclasses/AmbiguousShow.java new file mode 100644 index 0000000..06a39b3 --- /dev/null +++ b/src/test/java/com/garciat/typeclasses/testclasses/AmbiguousShow.java @@ -0,0 +1,19 @@ +package com.garciat.typeclasses.testclasses; + +import com.garciat.typeclasses.api.TypeClass; + +@TypeClass +public interface AmbiguousShow { + String show(A a); + + // Two witnesses without overlap markers - should cause ambiguity + @TypeClass.Witness + static AmbiguousShow witness1() { + return a -> "witness1"; + } + + @TypeClass.Witness + static AmbiguousShow witness2() { + return a -> "witness2"; + } +} diff --git a/src/test/java/com/garciat/typeclasses/testclasses/OverlapShow.java b/src/test/java/com/garciat/typeclasses/testclasses/OverlapShow.java new file mode 100644 index 0000000..a35248e --- /dev/null +++ b/src/test/java/com/garciat/typeclasses/testclasses/OverlapShow.java @@ -0,0 +1,23 @@ +package com.garciat.typeclasses.testclasses; + +import com.garciat.typeclasses.api.TypeClass; + +@TypeClass +public interface OverlapShow { + String show(A a); + + @TypeClass.Witness(overlap = TypeClass.Witness.Overlap.OVERLAPPABLE) + static OverlapShow genericShow() { + return a -> "Generic: " + a.toString(); + } + + @TypeClass.Witness(overlap = TypeClass.Witness.Overlap.OVERLAPPING) + static OverlapShow integerShow() { + return i -> "Integer: " + i; + } + + @TypeClass.Witness(overlap = TypeClass.Witness.Overlap.OVERLAPPING) + static OverlapShow stringShow() { + return s -> "String: " + s; + } +} diff --git a/src/test/java/com/garciat/typeclasses/testclasses/TestEq.java b/src/test/java/com/garciat/typeclasses/testclasses/TestEq.java new file mode 100644 index 0000000..cb8fbd2 --- /dev/null +++ b/src/test/java/com/garciat/typeclasses/testclasses/TestEq.java @@ -0,0 +1,50 @@ +package com.garciat.typeclasses.testclasses; + +import com.garciat.typeclasses.api.TypeClass; +import java.util.List; +import java.util.Map; + +@TypeClass +public interface TestEq { + boolean eq(A a1, A a2); + + @TypeClass.Witness + static TestEq stringEq() { + return String::equals; + } + + @TypeClass.Witness + static TestEq integerEq() { + return Integer::equals; + } + + @TypeClass.Witness + static TestEq> listEq(TestEq eqA) { + return (l1, l2) -> { + if (l1.size() != l2.size()) return false; + for (int i = 0; i < l1.size(); i++) { + if (!eqA.eq(l1.get(i), l2.get(i))) return false; + } + return true; + }; + } + + @TypeClass.Witness + static TestEq> mapEq(TestEq eqK, TestEq 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/test/java/com/garciat/typeclasses/testclasses/TestShow.java b/src/test/java/com/garciat/typeclasses/testclasses/TestShow.java new file mode 100644 index 0000000..9a6d317 --- /dev/null +++ b/src/test/java/com/garciat/typeclasses/testclasses/TestShow.java @@ -0,0 +1,35 @@ +package com.garciat.typeclasses.testclasses; + +import com.garciat.typeclasses.api.TypeClass; +import java.util.List; +import java.util.Optional; + +@TypeClass +public interface TestShow { + String show(A a); + + @TypeClass.Witness + static TestShow stringShow() { + return s -> "string:" + s; + } + + @TypeClass.Witness + static TestShow integerShow() { + return i -> "int:" + i; + } + + @TypeClass.Witness + static TestShow> optionalShow(TestShow showA) { + return optA -> optA.map(a -> "opt(" + showA.show(a) + ")").orElse("empty"); + } + + @TypeClass.Witness + static TestShow> listShow(TestShow showA) { + return listA -> + listA.stream() + .map(showA::show) + .reduce((a, b) -> a + "," + b) + .map(s -> "[" + s + "]") + .orElse("[]"); + } +}