skip to content

Functor, Applicative, Monad

The ladder from mapping inside a wrapper, to combining two wrappers, to chaining a step that depends on the last one. Interviewers want the difference between the rungs, not a category-theory recital.

on this pageshow

questions

4

When you map a lookup that itself returns a wrapped value over a wrapped value, what comes back, and which operation did you actually need?

level: middleimportance: must knowfreq 62%

answer

  1. count the layers you unwrap
  2. the step returns its own context
  3. mapping lifts, it never joins
  4. wrapper inside a wrapper is the symptom
  5. chaining absorbs one layer, depth stays one

basics

~20 s

Mapping a function that itself returns a wrapped value gives you a wrapper nested inside a wrapper. Chaining is the operation you needed: it runs the step and hands back one layer, absorbing the step's own context instead of stacking it.

solid answer

~50 s

The mapping rung takes a plain function `A -> B` and lifts it inside the context, so a `Wrapper<A>` becomes a `Wrapper<B>`. Hand it a context-producing function `A -> Wrapper<B>` instead, and `B` is instantiated as `Wrapper<B>`: the honest result is `Wrapper<Wrapper<B>>`. That nesting is the signal you needed the chaining rung, which is defined for exactly that shape and returns a single `Wrapper<B>` — the step's own context is joined with the outer one rather than stacked on it. In a configuration assembly where reading the host yields a wrapped value and resolving that host to an endpoint yields another one, mapping leaves the caller unwrapping twice and deciding what an outer success around an inner failure means; chaining keeps the pipeline one layer deep and stops at the first step that produces the empty or failed case.

code

pseudocode · 10 lines
pseudocode
readHost(source)        // returns Wrapped<Host>
resolveEndpoint(host)   // returns Wrapped<Endpoint>

// mapping: the step's own wrapper is kept, so layers stack
nested = map(readHost(source), host -> resolveEndpoint(host))
// nested : Wrapped<Wrapped<Endpoint>>

// chaining: the step's wrapper is absorbed, depth stays one
flat = chain(readHost(source), host -> resolveEndpoint(host))
// flat : Wrapped<Endpoint>

go deeper

for a junior

Recall the two shapes: mapping takes a plain function and gives back the same kind of wrapper, while a step that returns its own wrapper needs chaining instead.

for a middle

Explain the signatures — mapping takes a function from a value to a value, chaining takes a function from a value to a wrapped value — and show why substituting one for the other nests the wrapper.

for a senior

Show where nested wrappers appear on a real assembly path and what they cost: repeated unwrapping at every call site, a short-circuit nobody can rely on, and absence handling spreading outward.

for a principal

Weigh whether a shared context type in your codebase should offer chaining at all, since that rung lets any consumer make one step depend on another and pushes every pipeline into a single evaluation order.

## Three rungs, three shapes A **context** here is any wrapper that carries a value plus something extra: it may be empty, it may carry a described failure, it may stand for work that has not run yet. The three rungs of the ladder differ only in the shape of the function they accept and in what they do with the contexts involved. - **Mapping** takes a plain function `A -> B` together with a `Wrapper<A>` and returns a `Wrapper<B>`. It never consults the context's extra information and never changes it: whatever made the wrapper empty or failed is passed through untouched. - **Combining** takes several already-built contexts plus a plain function of their values and returns one context. The contexts were built independently, so none of them can see another's result. - **Chaining** takes a `Wrapper<A>` and a *context-producing* function `A -> Wrapper<B>`, and returns a `Wrapper<B>`. This is the only rung on which the second context is computed from the first one's value. | rung | function it takes | result | can the next step see the previous value? | |---|---|---|---| | mapping | `A -> B` | `Wrapper<B>` | there is no next context | | combining | `(A, B) -> C`, over contexts already built | `Wrapper<C>` | no | | chaining | `A -> Wrapper<B>` | `Wrapper<B>` | yes | ## Why the nesting happens Mapping's contract is mechanical: substitute the function's result type for the wrapper's type parameter. If the function you pass returns `Wrapper<B>`, then the `B` in `Wrapper<B>` is itself `Wrapper<B>`, and the result type is `Wrapper<Wrapper<B>>`. Nothing malfunctioned — mapping did precisely what it promises. The nesting is a receipt written in the type, saying *two contexts were produced here and nothing joined them*. Depth grows with the number of such steps. Three context-producing steps mapped in a row give a wrapper three layers deep: the caller unwraps three times before reaching a value, and in between has to answer an awkward question — what does an outer success wrapping an inner empty case actually mean? ## What chaining actually does Chaining is mapping followed by one join. Written out: 1. Apply the context-producing function inside the wrapper, which yields `Wrapper<Wrapper<B>>`. 2. Collapse those two layers into one, using the context's own rule for how its extra information merges. 3. Return a single `Wrapper<B>`. Step 2 is the entire content of the rung, and it is where short-circuiting comes from. If the outer context is empty or failed, the function was never applied and there is nothing to join. If the outer one held a value and the inner one is empty or failed, the joined result carries that. So a chain ends at the first step producing the empty-or-failed case, and the steps after it — functions waiting on a value that never arrives — are simply never applied. Nobody wrote a check; the shape of the operation performs it. ## Assembling a configuration Assembling one application configuration out of several independently loaded settings puts all three rungs in one pipeline: - Reading the raw host setting yields a wrapped value, because the setting may be absent. - **Trimming** that value is a plain function of the value, so it is the mapping rung and the depth does not change. - **Resolving** that host into an endpoint needs a second lookup that may itself be absent, so its signature is `Host -> Wrapper<Endpoint>` — the chaining rung. Map it instead and you are holding a wrapped wrapped endpoint. - Reading the port needs nothing from the host, so pairing host with port is the combining rung, not the chaining one. The tell in review is the signature of the function at each stage, never the name of the method: a step that returns a context needs chaining, a step that returns a plain value needs mapping. ## The trap this replaces The alternative most candidates reach for is to unwrap the outer context, test whether it holds a value, and then either re-wrap or return the empty case by hand. It works, and it costs three things: - Every call site repeats the test, so the decision about what *absent* means is duplicated wherever the value travels. - The short-circuit becomes something a reader has to verify branch by branch, instead of something the pipeline's shape guarantees. - One missed test turns an absent value into whatever the surrounding language does with an unwrapped empty context — exactly the failure the wrapper existed to prevent. Nesting, then, is not a cosmetic annoyance. It is the type telling you which rung the step needed, before the mistake reaches a caller.

  • If every step in the pipeline is context-producing, how deep does a purely mapped version get?
    One extra layer per step: three mapped context-producing steps give a wrapper three layers deep, and the caller unwraps three times before reaching a value. Depth grows linearly with the length of the pipeline, which is why the chaining rung exists — it holds depth at one however long the chain runs.
  • Can chaining always be recovered from mapping alone?
    Only when the context also offers a joining operation that collapses one nested layer, because chaining is exactly mapping followed by that join. A context that can map but cannot join is a functor and no more, and callers of it are left unwrapping the layers by hand.
  • In a chain of context-producing steps, which step decides whether the later ones run?
    The first one that produces the empty or failed case. The remaining steps are functions waiting on a value that never arrives, so they are never applied and the chain evaluates to that first empty-or-failed context. The short-circuit falls out of the chain's shape rather than being an added check.

Mapping is relabelling a sealed parcel without opening it. When the relabelling step hands you another parcel, chaining is what puts the contents back into one parcel instead of leaving you holding a parcel inside a parcel.

saying these in an interview costs you the question

  • Says mapping and chaining are two names for one operation
  • Claims mapping flattens a nested wrapper automatically
  • Unwraps the outer context, tests it, then re-wraps by hand
  • Thinks the nesting is a typing artifact with no consequence
  • Says chaining strips the context and returns a bare value
open as a page

Two configuration settings load independently, neither needing the other's value — which combining operation fits, and what does chaining them instead cost?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Independent loads belong to the combining rung, which takes contexts built side by side and a plain function of their values. Chaining them instead makes the second load a function of the first, so one evaluation order is forced and nothing from the second is available when the first is empty.

open as a page

Standardising how teams assemble configuration, you require the weakest of mapping, combining and chaining that expresses each step — what does that buy, and where does the rule cost more than it returns?

level: principalimportance: should knowfreq 28%

basics

~20 s

Requiring the weakest sufficient rung keeps real dependencies visible: a combining step advertises that neither load needs the other, so it may be evaluated in any order and both results stay reachable. It costs review effort, and it stops paying when the work is genuinely sequential.

open as a page

A teammate rewrites two consecutive mapping stages over a wrapped value as one — which law makes that safe, and when does it break?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

The functor composition law: mapping one function and then another must equal mapping the two as a single function in one pass. The rewrite is safe only while mapping touches nothing but the contained value — a mapping that also counts, logs or re-applies breaks it.

open as a page