Tutorial Solutions Guide
- When to consult solutions versus working through problems independently
- How to learn effectively from solutions without just copy-pasting code
- Common patterns used throughout solution files (widen-operate-narrow, typeclass instances, optic composition)
- Debugging techniques for compilation and runtime errors
- How to experiment with variations and connect solutions to documentation
What's in a Solution File?
Every solution file contains the working code and per-exercise teaching commentary. Each @Test method carries a Javadoc block written in this shape:
/**
* Why this is idiomatic: explains what makes the chosen form the standard one — the
* single sentence you'd give a reviewer who asked "why this and not the obvious thing?"
*
* <p>Alternative: at least one other shape that also works, with the trade-off named.
* Same answer; loses {something}.
*
* <p>Common wrong attempt: a typical first stumble and the symptom it produces.
* The compiler doesn't catch this; the test will.
*/
@Test
void exerciseN_someName() {
// working code that passes the test
}
That format is the same across all 60-odd solution files (Capstone, coretypes, optics, expression, effecthandlers, context, concurrency, transformers, resilience). When you peek at a solution, read the working code first, then the commentary — the prose tells you why the chosen form is preferred, what the close alternatives are, and which mistake the exercise is guarding against.
The pilot solutions live in coretypes/Tutorial01_KindBasics_Solution.java and optics/Tutorial01_LensBasics_Solution.java — they are the canonical references for the format.
Philosophy: When to Use Solutions
The solution files exist to help you learn, not to short-circuit the learning process. Here's how to use them effectively.
✅ Good Reasons to Check Solutions
- After Multiple Genuine Attempts: You've tried for 10+ minutes and exhausted your ideas
- To Verify Your Approach: You have a working solution but want to compare approaches
- To Learn Idioms: You want to see the "idiomatic" way to use the library
- When Completely Stuck: You're blocked on a fundamental concept and can't progress
❌ Poor Reasons to Check Solutions
- Immediately When Confused: Give yourself time to think through the problem
- To Save Time: The struggle is where learning happens; shortcuts lead to shallow understanding
- Copy-Pasting for Green Tests: You'll pass the tutorial but won't retain the knowledge
- Because It's Available: Resist the temptation!
Rule of Thumb: If you haven't spent at least 5 minutes thinking about the problem, you're not ready for the solution.
How to Learn from Solutions
When you do consult a solution, approach it systematically:
1. Don't Just Copy-Paste
Instead:
- Read the per-exercise Javadoc first — the Why this is idiomatic paragraph names what the working code is doing and why
- Read the working code with that framing in mind
- Skim the Alternative paragraph to understand a close-but-different shape
- Note the Common wrong attempt so you know what to avoid
- Close the solution file
- Re-implement it yourself from memory
- Run the test to verify understanding
2. Compare Approaches
If you have a working solution that differs from the provided one:
- Are they functionally equivalent?
- Is one more idiomatic?
- Is one more efficient?
- What trade-offs exist between them?
Example:
// Your solution (verbose but clear)
Either<String, Integer> result = value1.flatMap(a ->
value2.map(b -> a + b)
);
// Provided solution (using map2 - more idiomatic for Applicative)
MonadError<EitherKind.Witness<String>, String> applicative = Instances.monadError(either());
Either<String, Integer> result = EITHER.narrow(
applicative.map2(EITHER.widen(value1), EITHER.widen(value2), (a, b) -> a + b)
);
Both are correct, but the second uses the Applicative abstraction more idiomatically.
3. Identify Patterns
Solutions often reveal reusable patterns:
Pattern: Typeclass Access
// Pattern you'll see repeatedly
SomeMonad<ErrorType> monad = SomeMonad.instance();
ConcreteType<ErrorType, ValueType> result = HELPER.narrow(
monad.operationName(HELPER.widen(input), ...)
);
Pattern: Optic Composition
// Pattern: Build paths from small pieces
var outerToInner = OuterLenses.middle()
.andThen(MiddleLenses.inner())
.andThen(InnerLenses.field());
4. Annotate Solutions
When studying a solution, add your own comments explaining what each part does:
// Create the Applicative instance for Either with String errors
MonadError<EitherKind.Witness<String>, String> applicative = Instances.monadError(either());
// Widen both Either values to Kind for generic processing
// Combine them using map2 (because they're independent)
// Narrow the result back to concrete Either type
Either<String, Integer> result = EITHER.narrow(
applicative.map2(
EITHER.widen(value1), // First independent value
EITHER.widen(value2), // Second independent value
(a, b) -> a + b // Combining function
)
);
Understanding Common Solution Patterns
Pattern 1: Widen → Operate → Narrow
Why: Generic operations work on Kind<F, A>, not concrete types.
// 1. Start with concrete type
Either<String, Integer> either = Either.right(42);
// 2. Widen to Kind for generic operation
Kind<EitherKind.Witness<String>, Integer> kind = EITHER.widen(either);
// 3. Perform generic operation (e.g., Functor.map)
Kind<EitherKind.Witness<String>, String> mapped = functor.map(Object::toString, kind);
// 4. Narrow back to concrete type
Either<String, String> result = EITHER.narrow(mapped);
When you see this: Core Types tutorials use this pattern extensively.
Pattern 2: Typeclass Instance Retrieval
Why: Typeclasses provide the implementation for generic operations.
// Get the Monad instance for Either with String errors
MonadError<EitherKind.Witness<String>, String> monad = Instances.monadError(either());
// Use it to perform monadic operations
monad.flatMap(...);
When you see this: Tutorials 02-05 in Core Types track.
Pattern 3: Optic Composition Chains
Why: Small, focused optics compose into powerful transformations.
// Build a path through nested structures
var leagueToPlayerScores = LeagueTraversals.teams() // League → Teams
.andThen(TeamTraversals.players()) // Team → Players
.andThen(PlayerLenses.score().asTraversal()); // Player → Score
When you see this: Optics tutorials 02, 04, 05, 07.
Pattern 4: Manual Lens Creation
Why: Annotation processor can't generate lenses for local classes.
class ProductLenses {
public static Lens<Product, String> name() {
return Lens.of(
Product::name, // Getter
(product, newName) -> new Product( // Setter
product.id(),
newName,
product.price()
)
);
}
}
When you see this: Optics tutorials with local record definitions.
Pattern 5: Traversal Creation
Why: Custom containers need custom traversals.
public static Traversal<Order, LineItem> items() {
return new Traversal<>() {
@Override
public <F> Kind<F, Order> modifyF(
Function<LineItem, Kind<F, LineItem>> f,
Order order,
Applicative<F> applicative
) {
// Traverse items list, applying f to each element
Kind<F, List<LineItem>> updatedItems =
ListTraverse.instance().traverse(applicative, f, order.items());
// Map the result back to Order
return applicative.map(
newItems -> new Order(order.id(), newItems, order.status()),
updatedItems
);
}
};
}
When you see this: Optics Tutorial 07.
Debugging Your Solutions
Common Compilation Errors
Error: "cannot find symbol: method answerRequired()"
Cause: You haven't imported or defined the helper method.
Fix: Ensure this exists at the top of the file:
private static <T> T answerRequired() {
throw new RuntimeException("Answer required");
}
Error: "incompatible types: ... cannot be converted to Kind<...>"
Cause: Forgot to widen before passing to generic code.
Fix: Wrap with the appropriate helper:
EITHER.widen(eitherValue)
MAYBE.widen(maybeValue)
LIST.widen(listValue)
Error: "cannot find symbol: variable SomeLenses"
Cause: Annotation processor hasn't run or class isn't eligible for generation.
Fix:
- Rebuild project:
./gradlew clean build - Check annotation is on top-level or static class (not local class)
- Verify
@GenerateLensesimport is correct
Error: "method mapN in interface Applicative cannot be applied"
Cause: Wrong number of arguments or incorrect type parameters.
Fix: Check you're using the right map2/map3/map4/map5 for the number of values you're combining.
Common Runtime Errors
Error: "Answer required" exception
Cause: You haven't replaced the placeholder with a solution.
Fix: This is expected! Replace answerRequired() with working code.
Error: "KindUnwrapException"
Cause: Trying to narrow a Kind<F, A> that wasn't created from the expected type.
Fix: Ensure the witness type matches. EITHER.narrow() only works on Kind<EitherKind.Witness<L>, R>.
Error: NullPointerException in Free Monad validation
Cause: Validation interpreter returns null for get operations.
Fix: Add null checks:
.flatMap(value -> {
if (value != null && value.equals("expected")) {
// ...
} else {
// ...
}
})
Solution File Organisation
Solutions mirror the tutorial structure under hkj-examples/src/test/java/org/higherkindedj/tutorial/solutions/. The contents below are kept in lockstep with the tutorial directories; if we spot drift, please open an issue.
solutions/
├── coretypes/ (11 solutions)
│ ├── Tutorial01_KindBasics_Solution.java
│ ├── Tutorial02_FunctorMapping_Solution.java
│ ├── Tutorial03_ApplicativeCombining_Solution.java
│ ├── Tutorial04_MonadChaining_Solution.java
│ ├── Tutorial05_MonadErrorHandling_Solution.java
│ ├── Tutorial06_ConcreteTypes_Solution.java
│ ├── Tutorial07_RealWorld_Solution.java
│ ├── Tutorial08_NaturalTransformation_Solution.java
│ ├── Tutorial09_Coyoneda_Solution.java
│ ├── Tutorial10_FreeApplicative_Solution.java
│ └── Tutorial11_StaticAnalysis_Solution.java
├── effect/ (2 solutions)
│ ├── Tutorial01_EffectPathBasics_Solution.java
│ └── Tutorial02_EffectPathAdvanced_Solution.java
├── transformers/ (4 solutions)
│ ├── Tutorial01_WhenPathIsNotEnough_Solution.java
│ ├── Tutorial02_AsyncWithAbsence_Solution.java
│ ├── Tutorial03_StackingTransformers_Solution.java
│ └── Tutorial04_PolymorphicCapabilities_Solution.java
├── concurrency/ (8 solutions)
│ ├── TutorialVTask_Solution.java
│ ├── TutorialVTaskPath_Solution.java
│ ├── TutorialVTaskForPath_Solution.java
│ ├── TutorialVStream_Solution.java
│ ├── TutorialVStreamHKT_Solution.java
│ ├── TutorialVStreamParallel_Solution.java
│ ├── TutorialVStreamPath_Solution.java
│ └── TutorialVStreamAdvanced_Solution.java
├── optics/ (20 solutions)
│ ├── Tutorial01_LensBasics_Solution.java
│ ├── Tutorial02_LensComposition_Solution.java
│ ├── Tutorial03_PrismBasics_Solution.java
│ ├── Tutorial04_AffineBasics_Solution.java
│ ├── Tutorial05_TraversalBasics_Solution.java
│ ├── Tutorial06_OpticsComposition_Solution.java
│ ├── Tutorial07_GeneratedOptics_Solution.java
│ ├── Tutorial08_RealWorldOptics_Solution.java
│ ├── Tutorial09_FluentOpticsAPI_Solution.java
│ ├── Tutorial10_AdvancedPrismPatterns_Solution.java
│ ├── Tutorial11_AdvancedOpticsDSL_Solution.java
│ ├── Tutorial12_FocusDSL_Solution.java
│ ├── Tutorial13_AdvancedFocusDSL_Solution.java
│ ├── Tutorial14_FocusEffectBridge_Solution.java
│ ├── Tutorial15_ListPrisms_Solution.java
│ ├── Tutorial16_OpticsSpecInterfaces_Solution.java
│ ├── Tutorial17_VStreamOptics_Solution.java
│ ├── Tutorial18_FoldCombination_Solution.java
│ ├── Tutorial19_NavigatorGeneration_Solution.java
│ └── Tutorial20_ContainerNavigation_Solution.java
├── expression/ (4 solutions)
│ ├── Tutorial01_ForStateBasics_Solution.java
│ ├── Tutorial02_ForPathParallel_Solution.java
│ ├── Tutorial03_ForTraverseComprehension_Solution.java
│ └── Tutorial04_EnhancedOpticsIntegration_Solution.java
├── context/ (6 solutions)
│ ├── Tutorial01_ContextBasics_Solution.java
│ ├── Tutorial02_ContextComposition_Solution.java
│ ├── Tutorial03_RequestContextPatterns_Solution.java
│ ├── Tutorial04_SecurityContextPatterns_Solution.java
│ ├── Tutorial05_ContextWithVTask_Solution.java
│ └── Tutorial06_AdvancedContextPatterns_Solution.java
├── effecthandlers/ (6 solutions)
│ ├── Tutorial01_EffectAlgebraBasics_Solution.java
│ ├── Tutorial02_MultipleInterpreters_Solution.java
│ ├── Tutorial03_ErrorRecovery_Solution.java
│ ├── Tutorial04_CombiningEffects_Solution.java
│ ├── Tutorial05_ProgramInspection_Solution.java
│ └── Tutorial06_AdvancedInterpreters_Solution.java
└── resilience/ (4 solutions)
├── Tutorial01_CircuitBreaker_Solution.java
├── Tutorial02_Saga_Solution.java
├── Tutorial03_RetryBulkheadResilience_Solution.java
└── Tutorial04_PathResilience_Solution.java
Each solution file:
- Contains complete, working implementations for all exercises
- Carries a per-exercise Javadoc block in the Why this is idiomatic / Alternative / Common wrong attempt format
- Demonstrates idiomatic usage patterns
- Compiles and passes all tests
Learning Beyond Solutions
Experiment with Variations
Once you understand a solution, try variations:
Original solution:
Either<String, Integer> result = EITHER.narrow(
applicative.map2(EITHER.widen(value1), EITHER.widen(value2), (a, b) -> a + b)
);
Variations to try:
- What if one value is
Left? (Test the error path) - Can you use
flatMapinstead? (Understand the difference) - How would this work with
Validatedinstead ofEither? (See error accumulation)
Connect to Documentation
Each solution references specific documentation sections. Follow these links to deepen understanding:
- If a solution uses
Functor.map→ Read the Functor Guide - If a solution composes optics → Read Composing Optics
- If a solution uses a specific monad → Read its dedicated guide
Build Your Own Exercises
After mastering the tutorials, create your own scenarios:
- Define a domain model from your work
- Write tests that require optics or monads
- Solve them using the patterns you've learned
- Compare your solutions to the tutorial patterns
When Solutions Don't Help
If you've read the solution but still don't understand:
- Go back to fundamentals: Re-read the introduction to that tutorial
- Check prerequisites: Maybe you need to understand an earlier concept first
- Read the documentation: Solutions show how, documentation explains why
- Ask for help: Use GitHub Discussions
Remember
The solution is not the destination; understanding is.
A copied solution gets you a green test today but leaves you unprepared for tomorrow's challenges. The effort you put into solving exercises yourself directly translates to your ability to apply these patterns in production code.
Take your time. Struggle productively. Learn deeply.
Previous: Learning Paths Next: Troubleshooting