diff --git a/src/main/java/com/garciat/typeclasses/TypeClasses.java b/src/main/java/com/garciat/typeclasses/TypeClasses.java index 3a712fd..a7ebec6 100644 --- a/src/main/java/com/garciat/typeclasses/TypeClasses.java +++ b/src/main/java/com/garciat/typeclasses/TypeClasses.java @@ -46,26 +46,26 @@ private WitnessResolutionException(SummonError error) { } } - private sealed interface SummonError { - record NotFound(ParsedType target) implements SummonError {} + private sealed interface ResolutionError { + record NotFound(ParsedType target) implements ResolutionError {} - record Ambiguous(ParsedType target, List candidates) implements SummonError {} + record Ambiguous(ParsedType target, List candidates) implements ResolutionError {} - record Nested(ParsedType target, SummonError cause) implements SummonError {} + record Nested(ParsedType target, ResolutionError cause) implements ResolutionError {} default String format() { return switch (this) { case NotFound(ParsedType target) -> "No witness found for type: " + target.format(); - case Ambiguous(ParsedType target, List candidates) -> + case Ambiguous(ParsedType target, List candidates) -> "Ambiguous witnesses found for type: " + target.format() + "\nCandidates:\n" + candidates.stream() - .map(c -> c.rule().toString()) + .map(WitnessRule::toString) .collect(Collectors.joining("\n")) .indent(2); - case Nested(ParsedType target, SummonError cause) -> - "While summoning witness for type: " + case Nested(ParsedType target, ResolutionError cause) -> + "While resolving witness for type: " + target.format() + "\nCaused by: " + cause.format().indent(2); @@ -73,36 +73,145 @@ case Nested(ParsedType target, SummonError cause) -> } } + private sealed interface InstantiationError { + record LookupMiss(ParsedType type) implements InstantiationError {} + + record InvocationException(Method method, Exception cause) implements InstantiationError {} + + default String format() { + return switch (this) { + case LookupMiss(ParsedType type) -> "Context lookup failed for type: " + type.format(); + case InvocationException(Method method, Exception cause) -> + "Failed to invoke constructor: " + method + "\nCause: " + cause.getMessage(); + }; + } + } + + private sealed interface SummonError { + record Resolution(ResolutionError error) implements SummonError {} + + record Instantiation(InstantiationError error) implements SummonError {} + + default String format() { + return switch (this) { + case Resolution(ResolutionError error) -> error.format(); + case Instantiation(InstantiationError error) -> error.format(); + }; + } + } + + /** + * Summons a witness for the given target type using the staged resolution approach: parseType >> + * resolveWitness >> compile >> interpret + */ 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)); + Either resolutionResult = + resolveWitness(target, context).mapLeft(SummonError.Resolution::new); + + Either> compilationResult = + resolutionResult.map(TypeClasses::compile); + + return compilationResult.flatMap( + expr -> + interpret(new InterpretContext(context), expr).mapLeft(SummonError.Instantiation::new)); + } + + // ========== Staged Resolution Components ========== + + /** + * Represents the fully resolved instantiation plan. This is a tree structure where each node is a + * step in the instantiation process, with dependencies on other steps. + */ + private sealed interface InstantiationPlan { + record PlanStep(WitnessRule target, List dependencies) + implements InstantiationPlan {} + } + + /** + * Represents a reduced AST of interpretable Java operations. This is the compiled form of an + * InstantiationPlan. + */ + private sealed interface Expr { + record InvokeConstructor(Method method, List> arguments) implements Expr {} + + record Lookup(K key) implements Expr {} + } + + /** Resolves a ParsedType into an InstantiationPlan. */ + private static Either resolveWitness( + ParsedType target, List context) { + record Match(WitnessRule rule, List requirements) {} + + List matches = + Stream.concat(context.stream(), reduceOverlapping(findRules(target)).stream()) + .flatMap( + rule -> + rule + .tryMatch(target) + .map(requirements -> new Match(rule, requirements)) + .stream()) + .toList(); + + return switch (ZeroOneMore.of(matches)) { + case ZeroOneMore.One(Match(var rule, var requirements)) -> + resolveWitnessAll(requirements, context) + .map( + dependencies -> new InstantiationPlan.PlanStep(rule, dependencies)) + .mapLeft(error -> new ResolutionError.Nested(target, error)); + case ZeroOneMore.Zero() -> Either.left(new ResolutionError.NotFound(target)); + case ZeroOneMore.More(var matches2) -> + Either.left( + new ResolutionError.Ambiguous(target, matches2.stream().map(Match::rule).toList())); }; } - private static Either> summonAll( + /** Resolves multiple ParsedTypes into a list of InstantiationPlans. */ + private static Either> resolveWitnessAll( List targets, List context) { - return Either.traverse(targets, target -> summon(target, context)); + return Either.traverse(targets, target -> resolveWitness(target, context)); } - private record Candidate(WitnessRule rule, List requirements) {} + /** Compiles an InstantiationPlan into an Expr. */ + private static Expr compile(InstantiationPlan plan) { + return switch (plan) { + case InstantiationPlan.PlanStep(var rule, var dependencies) -> + switch (rule) { + case ContextInstance(var instance, var type) -> new Expr.Lookup<>(type); + case InstanceConstructor(var func) -> + new Expr.InvokeConstructor<>( + func.java(), dependencies.stream().map(TypeClasses::compile).toList()); + }; + }; + } - 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(); + /** + * Context for interpretation - maps keys to resolved instances. For our use case, keys are + * ParsedTypes. + */ + private record InterpretContext(List instances) { + Either lookup(ParsedType type) { + return instances.stream() + .filter(ci -> ci.type().equals(type)) + .findFirst() + .map(ContextInstance::instance) + .>map(Either::right) + .orElseGet(() -> Either.left(new InstantiationError.LookupMiss(type))); + } + } + + /** Interprets an Expr with a given context. */ + private static Either interpret( + InterpretContext context, Expr expr) { + return switch (expr) { + case Expr.Lookup(var type) -> context.lookup(type); + case Expr.InvokeConstructor(var method, var args) -> + Either.traverse(args, arg -> interpret(context, arg)) + .flatMap( + argValues -> + Either.call(() -> method.invoke(null, argValues.toArray())) + .mapLeft(e -> new InstantiationError.InvocationException(method, e))); + }; } /** @@ -156,8 +265,6 @@ private static boolean isWitnessMethod(Method m) { private sealed interface WitnessRule { Maybe> tryMatch(ParsedType target); - - Object instantiate(List dependencies); } private record ContextInstance(Object instance, ParsedType type) implements WitnessRule { @@ -166,11 +273,6 @@ 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(); @@ -188,15 +290,6 @@ public Maybe> tryMatch(ParsedType 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/types/Either.java b/src/main/java/com/garciat/typeclasses/types/Either.java index 2cba8a5..f470aa8 100644 --- a/src/main/java/com/garciat/typeclasses/types/Either.java +++ b/src/main/java/com/garciat/typeclasses/types/Either.java @@ -10,6 +10,7 @@ import com.garciat.typeclasses.classes.Functor; import com.garciat.typeclasses.classes.Monad; import java.util.List; +import java.util.concurrent.Callable; import java.util.function.Function; public sealed interface Either extends TApp, R> { @@ -25,6 +26,14 @@ static Either right(R value) { return new Right<>(value); } + static Either call(Callable callable) { + try { + return right(callable.call()); + } catch (Exception e) { + return left(e); + } + } + default Either map(Function f) { return fold(Either::left, f.andThen(Either::right)); }