Foundations Journey — Cheatsheet

A single-page reference for the four tutorials in the Foundations journey. Two columns: how we wrote it before, how we write it now.

Scope

  • Kind, Functor, Applicative, Monad
  • Either, List, Maybe, Validated
  • The widen / call / narrow pattern at the typeclass layer

Kind, widen, narrow (Tutorial 01)

PatternImperative JavaHigher-Kinded-J
Reach for a generic over containersOne method per container, foreverKind<F, A> and a Functor/Monad instance
Lift a concrete type to Kindn/aEITHER.widen(either) / LIST.widen(list) / etc.
Drop a Kind back to the concrete typen/aEITHER.narrow(kind) / LIST.narrow(kind) / etc.
Talk about "the shape F" at the type levelnot expressiblethe witness type, e.g. EitherKind.Witness<L>

Common stumble: narrowing with the wrong helper. The witness type is exactly what catches this — it is a compile error, not a runtime surprise.


Functor — map (Tutorial 02)

PatternImperative JavaHigher-Kinded-J
Transform every element of a listxs.stream().map(f).toList()monad.map(f, LIST.widen(xs))
Transform a possibly-absent valueopt.map(f)monad.map(f, MAYBE.widen(maybe))
Transform the success side of a resultresult.map(f) (custom Result)either.map(f) or monad.map(f, EITHER.widen(either))
Transform a futurefuture.thenApply(f)monad.map(f, FUTURE.widen(future))
Method reference inside a transformString::toUpperCaseunchanged — works exactly the same

Common stumble: using map with a function that returns a wrapped value. The result is F<F<B>> (nested). The fix is flatMap — see Tutorial 04.


Applicative — map2map5 (Tutorial 03)

PatternImperative JavaHigher-Kinded-J
Combine N independent resultsmanual conditional ladderapp.map2(...) ... app.map5(...) on the typeclass instance
Lift a plain value into a containerEither.right(x), Optional.of(x)app.of(x) or the concrete factory
Run N futures and combine their outputsCompletableFuture.allOf(...) then unpackapp.map2(widen(f1), widen(f2), combiner)
Validate a form, fail-fastseries of early returnsEitherMonad + mapN (short-circuits on first Left)
Validate a form, accumulate errorsSet<ConstraintViolation> from Bean ValidationValidatedMonad + mapN (the Semigroup decides how to combine)

Common stumble: calling value1.map2(value2, ...) directly on the concrete type. Either does not carry map2 as an instance method. Combinators across multiple inputs live on the Applicative typeclass instance: get the instance, widen, call, narrow.


Monad — flatMap (Tutorial 04)

PatternImperative JavaHigher-Kinded-J
Chain a step that depends on the previous oneif (e.isError()) return e; ladders.flatMap(...)
Parse → validate → computenested try/catch.flatMap(parse).flatMap(validate).flatMap(compute)
Look up X then look up Y(X) on Optionalopt.flatMap(...).flatMap(...) (same shape)
Compose async stepsfuture.thenCompose(...).flatMap(...) (same shape, on CompletableFuture)
Cartesian product of two listsnested for loops + buildermonad.flatMap(x -> monad.map(y -> ..., ys), xs)

Common stumble: using flatMap when steps are independent. flatMap says "this depends on that"; using it for independent inputs forces a sequential mental model and (on Validated) loses the accumulating semantics. Reach for mapN instead.


The widen / call / narrow pattern

Whenever we move from a concrete type into typeclass-generic code, we follow three steps:

   1. widen     EITHER.widen(either)            // Either<L, A>     -> Kind<EitherKind.Witness<L>, A>
   2. operate   monad.map(f, kind)              // operate at the Kind level
   3. narrow    EITHER.narrow(result)           // Kind<...>        -> Either<L, B>

Same shape for LIST/MAYBE/VALIDATED/OPTIONAL/FUTURE/etc. Once we know it for one container, we know it for every container.


Decision table

Are the steps independent?Can a step decide the next one?Reach for
Yesn/aApplicative (map2 / mapN)
NoYesMonad (flatMap)
Yes, but want all errors backn/aApplicative on Validated
Yes, fail fast on first errorn/aApplicative on Either
Single transformationn/aFunctor (map)
Just need to package up a valuen/aApplicative.of (or the concrete factory)

Where this lands in One Line, Six Layers

   repo.find(id)              .toEitherPath()      .focus().attributes().at(key)
   └── Effect Path ───────────┤                    └── Optic ──────────────────┐
       absence as MaybePath   │                        traversal into a record │
                              │                                                │
                              └── Natural transformation                       │
                                  MaybePath ~> EitherPath                      │
                                                                               │
   .modify(spec::validateAndCoerce)             .flatMap(repo::save);          │
   └── Functor (under the optic) ───┐           └── Monad ─────────────────────┘
       Tutorial 02                   │               Tutorial 04
                                    │
                                    └── Type class instance dispatched at compile time
                                        EitherFunctor / EitherMonad
                                        Tutorials 02-04

Tutorials 02-04 cover the lower three layers of the One Line, Six Layers diagram. Tutorial 00 walks through every layer end-to-end as a single working expression.


See also: Functor · Applicative · Monad · Lifting the Hood · Foundations FAQ