skip to content

Function Composition

Assembling behaviour by feeding one function's output into the next, including direction, associativity and identity. Interviewers use it to see if you build from small pieces instead of subclassing.

on this pageshow

questions

17

In a right-to-left composition written compose(compress, watermark, resize), which stage sees the raw photo first?

level: juniorimportance: must knowfreq 64%

answer

  1. same wiring, two listings
  2. listing order is a convention
  3. reading order versus run order
  4. rightmost stage touches the argument
  5. compose mirrors nested calls

basics

~10 s

Resize sees it first. A right-to-left combinator applies the last-listed function to the argument and feeds each result leftwards, so the stages are listed in the reverse of the order they run.

solid answer

~30 s

A right-to-left `compose` hands its argument to the **rightmost** function, then passes that result to the one on its left, and so on. So `compose(compress, watermark, resize)` resizes the photo, watermarks the resized image, and compresses last - the listing reads backwards from the run order. The left-to-right form of the same chain is `pipe(resize, watermark, compress)`, where listed order and run order agree. The wiring, the result and the number of stages are identical; only the listing convention differs. Both forms build a new one-argument function, and no stage touches a photo until that function is called with one.

code

pseudocode · 9 lines
pseudocode
function compose(f, g, h)
    return function(x) { return f(g(h(x))) }

function pipe(f, g, h)
    return function(x) { return h(g(f(x))) }

stored = compose(compress, watermark, resize)(photo)
same   = pipe(resize, watermark, compress)(photo)
// both: resize runs first, compress runs last

go deeper

for a junior

Recall that a composition helper's listing order is a convention, not the run order. Be able to point at the stage that touches the input and say why before tracing anything else.

for a middle

Explain the nesting: a right-to-left listing is the nested-call form with the argument removed, so the rightmost stage is the innermost call. Show that the left-to-right form rewires nothing and only reorders the line.

for a senior

Demonstrate how you confirm a helper's direction in code you did not write - reading its implementation, or applying it to two stages with distinguishable effects - rather than trusting the name or the convention you are used to.

for a principal

Frame direction as a convention whose cost is paid on every read and every review, not as a correctness question, and be ready to say what you would standardise on and why.

A photo upload is handled in three steps: **resize** the original, **watermark** the resized image, then **compress** the result for storage. There are two conventional ways to write that wiring as one function, and they disagree about the order the stages appear on the line. ## The two listings - **Right-to-left composition**, usually called `compose`: `compose(compress, watermark, resize)`. The combinator applies the **rightmost** function to the argument, hands that result to the function on its left, and keeps moving left. The listing is the reverse of the run order. - **Left-to-right piping**, usually called `pipe`: `pipe(resize, watermark, compress)`. The combinator applies the **leftmost** function to the argument and walks rightwards. The listing is the run order. Both calls build **one new function of one argument**. Handed the same photo, both produce the same bytes, run the same three stages, and do the same work. The only difference is which end of the written line the data enters. ## Trace it once 1. The built function is called with the original photo. 2. `resize` runs on the original and returns a smaller photo. 3. `watermark` runs on the **smaller** photo - so the mark is sized for the resized image, not the original. 4. `compress` runs on the watermarked photo and returns what is stored. That trace is identical for both listings. If a candidate reads `compose(compress, watermark, resize)` left to right and concludes the original is compressed first, they have read the listing as the run order. ## Reading order against application order | | `compose(compress, watermark, resize)` | `pipe(resize, watermark, compress)` | |---|---|---| | Stage that receives the photo | `resize`, the rightmost | `resize`, the leftmost | | Listing versus run order | reverse of it | the same as it | | Spoken without ambiguity | "compress **after** watermark **after** resize" | "resize, **then** watermark, **then** compress" | | Shape it mirrors | nested calls read outward | steps read in sequence | The words **after** and **then** are the cheapest fix for the ambiguity: "compose these three" says nothing about direction, while "compress after watermark" pins it. ## Why the right-to-left convention exists at all Write the chain out as ordinary nested application and it reads `compress(watermark(resize(photo)))`. The innermost call runs first, so reading the line left to right gives you the **last** stage first. Right-to-left composition preserves exactly that order: it is the nested form with the parentheses and the argument removed, which is why the stage nearest the (absent) argument sits on the right. Left-to-right piping abandons that heritage in favour of the order the data actually moves, which is the order a reader traces when debugging. ## What does not change with direction - The **result**: the same photo goes in and the same bytes come out. - The **join requirement**: each stage's result still has to be acceptable to the stage that receives it next. - The **cost**: the same three stages run once each; direction is a notation, not an optimisation. - The **moment work happens**: composing produces a function; the stages run when that function is applied to a photo. What does change is the reading cost. In a right-to-left listing, the reader's eye moves opposite to the data, and in a long chain that reversal is where mistakes get made - especially in a codebase where both conventions are present and a listing written for one helper is handed to the other. ## What an interviewer is listening for The checkable part of the answer is one sentence: **the rightmost stage in a right-to-left composition is the one that touches the input**. The better answer adds that this is a convention of the helper, not a law, so in unfamiliar code you confirm it - read the helper's few lines, or apply it to a single-stage chain and see which end moves - instead of assuming the convention you happen to be used to. Candidates who say "it depends which helper this is, and here is how I would check" are demonstrating the habit that keeps mixed-direction codebases from producing silently wrong images.

  • Does building the chain do any work to the photo?
    No. Both combinators return a new function of one argument; the three stages run only when that function is applied to a photo. Building it a hundred times resizes nothing. That is why a wiring mistake in the listing shows up as a wrong image later, at the call site, rather than as a failure where the chain was assembled.
  • How would you say the chain aloud so the direction is unambiguous?
    Use the joining word to carry the direction: "compress after watermark after resize" for a right-to-left listing, "resize, then watermark, then compress" for a left-to-right one. "Compose compress, watermark and resize" carries no direction at all, which is exactly how a reviewer ends up approving a reversed chain.
  • You open unfamiliar code and cannot tell which direction its helper uses - what do you do?
    Read the helper; it is usually three lines and the nesting answers it outright. Failing that, apply it to two stages whose effects are distinguishable - one that appends a marker and one that truncates - and see which marker survives. Guessing from the name is unreliable, because names for both conventions vary between codebases.

Reading a right-to-left composition is like reading an address on an envelope: the street comes first but the letter reaches the country first. Same journey, written from the wrong end.

saying these in an interview costs you the question

  • Says compose and pipe produce different final results
  • Claims the first function listed always runs first
  • Cannot say which listed stage receives the original argument
  • Assumes every codebase's compose runs left to right
  • Thinks assembling the chain already processed the photo
open as a page

In a job-board candidate filter, what does an 'and' combinator over two named predicates return - a boolean or another predicate?

level: juniorimportance: must knowfreq 62%

basics

~10 s

Another predicate. A combinator builds a condition rather than evaluating one: nothing is tested until the assembled predicate is finally applied to a candidate, and then it asks both questions and combines the answers.

open as a page

Why can the stages of a composed text-normalisation pipeline be regrouped freely but never swapped with each other?

level: middleimportance: must knowfreq 64%

basics

~20 s

Composition is associative but not commutative. Regrouping only changes which adjacent stages get bracketed together, so every stage still receives the same input; swapping two changes what the later one is handed, and therefore the result.

open as a page

How do you build a result ordering like 'best match first, then earliest application' from small comparators rather than one comparison function?

level: middleimportance: must knowfreq 58%

basics

~20 s

Give each comparator one key and one direction, then chain them: the next comparator is consulted only when the previous one reports a tie. Each returns a three-way verdict - before, equal, after - and 'equal' is the signal that hands control to the tiebreaker.

open as a page

In a composed text-normalisation pipeline, when is a stage that returns its input unchanged genuinely useful?

level: juniorimportance: should knowfreq 50%

basics

~20 s

A stage returning its input unchanged is the identity function: composing it changes nothing. It earns its place as a neutral default - what an optional stage falls back to, and the starting point when a list of stages is combined into one.

open as a page

When you rewrite a right-to-left photo pipeline as a left-to-right pipe, what must match between adjacent stages?

level: middleimportance: should knowfreq 48%

basics

~10 s

Each stage's result must be something the stage that receives it next accepts. That join requirement is identical in both directions; only the direction you read it along the written listing changes.

open as a page

In extractLevel(line) = uppercase(trim(fourthField(line))), when may the parameter line be dropped to give a point-free definition?

level: middleimportance: should knowfreq 40%

basics

~20 s

When the parameter occurs exactly once and is the argument the whole chain is applied to. The body is then just a chain applied to the name, so the name can be erased and the definition becomes the chain itself.

open as a page

Your team wants every log-parsing helper written point-free; where do you push back?

level: middleimportance: should knowfreq 45%

basics

~20 s

Where erasing the argument forces in combinators that only route the value around. Dropping a name that merely forwards removes noise; dropping a name that the body actually uses trades one clear word for machinery.

open as a page

A job board must list the candidates its filter rejected; why is negating a two-part 'and' filter not simply negating both parts?

level: middleimportance: should knowfreq 54%

basics

~20 s

Negation flips the connective as well as the parts. The complement of 'permit and three years' is 'no permit OR under three years', not 'no permit AND under three years' - the second selects only the candidates who fail both tests.

open as a page

Two teams each ship half of a text-normalisation pipeline as one composed stage - what guarantees the combined result is unchanged?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Associativity: splitting a chain into two composed halves is regrouping, and regrouping never changes the computed value. It guarantees only the value - not where a failure surfaces, when each half runs, or that the two halves stay the same stages over time.

open as a page

A module exposes both a right-to-left compose and a left-to-right pipe, and one photo chain watermarks before it resizes - how do you find and prevent that mistake?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Find it by asserting the chain's observable output, not its wiring: a listing written for one helper and passed to the other runs backwards while every join still fits. Prevent it by keeping one direction per codebase and naming the helper after its direction.

open as a page

A point-free log extractor fails on one malformed line; what has the missing parameter name cost you while diagnosing it?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Every value between the stages is anonymous, so there is no binding to inspect and no boundary carrying a domain label. Locating the failing stage means re-expanding the definition or inserting a pass-through probe first.

open as a page

A candidate filter combines a cheap stored-field test with an expensive computed-score test using 'and' - what does the order you combine them in decide?

level: seniorimportance: should knowfreq 44%

basics

~20 s

It decides how often the expensive test runs. A conjunction is settled by the first part that fails, so a cheap, highly rejecting test placed first keeps the score computation off most candidates - the accepted set is the same either way, the work is not.

open as a page

A job board lets a recruiter tick any subset of filter criteria; how do you assemble the matching condition at run time?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

Map each ticked criterion to its named predicate, collect the chosen ones into a list, and fold the list into a single condition with the conjunction combinator. Ticking nothing folds to a condition that accepts every candidate.

open as a page

Text-to-text normalisation stages compose into another such stage - which algebraic structure is that, and why does it matter?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Functions from a type to itself form a monoid under composition: an associative operation with the identity function as neutral element. So any list of stages, including the empty list, collapses into one stage by a single uniform rule.

open as a page

Your codebase uses both composition directions in different modules - how do you settle on one direction and migrate to it?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

Neither direction is more correct, so decide on reading cost and existing weight: pick the one most chains and reviewers already use, put the direction in the helper's name, then migrate module by module behind tests of each chain's output rather than in one sweep.

open as a page

How would you set a codebase-wide standard for point-free style when half the team reads it fluently and half does not?

level: principalimportance: nice to knowfreq 24%

basics

~20 s

Write a rule that a reviewer can check on a diff rather than one that asks for judgement, scope it to where the code is read most, and leave existing code alone. A standard nobody can apply gets relitigated every review.

open as a page