skip to content

Point-Free Style

Writing a definition as a chain of combinations with no argument named, and knowing when that clarifies and when it obscures. Interviewers use it to see if you weigh cleverness against readability.

on this pageshow

questions

4

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%

answer

  1. a rewrite, not a new function
  2. count the parameter's occurrences
  3. must be the last thing applied
  4. eta-reduction erases a forwarding name
  5. two uses need a combinator

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.

solid answer

~40 s

Point-free means the definition names no argument at all. That is possible here because `line` does exactly one thing: it is handed to `fourthField`, and everything after that works on results, not on `line`. So the body is one chain applied to the parameter, and a definition of the form `f(x) = chain(x)` can be rewritten as `f = chain` — the classic eta-reduction. Three conditions have to hold: the parameter occurs once, that occurrence is the argument the outermost chain is applied to, and nothing else in the body depends on it. Break any of them — use it twice, pass it as a stage's second argument, branch on it — and you cannot simply delete the name; you have to introduce a combinator whose only job is to move the value around.

code

pseudocode · 9 lines
pseudocode
// pointed: the argument is named once and only passed on
define extractLevel(line) = uppercase(trim(fourthField(line)))

// name the chain: apply fourthField, then trim, then uppercase
define levelPipeline = fourthField then trim then uppercase
define extractLevel(line) = levelPipeline(line)

// the parameter now does nothing but forward, so erase it
define extractLevel = levelPipeline

go deeper

for a junior

Know that a point-free definition names no argument: it states that the function equals a combination of other functions, and the value it will receive is never mentioned.

for a middle

Be able to state the erasability condition and perform the rewrite in both directions, and to spot a body where the parameter is used twice or sits in a non-final argument slot.

for a senior

Judge when a body cannot lose its name without adding routing machinery, and say plainly what the team pays for forcing the style there.

for a principal

Weigh argument-free definitions against the mix of readers a shared codebase has, and decide where the removed names were noise and where they were the documentation.

## What "point-free" actually claims A definition is **point-free** when it describes the function as a combination of other functions and never mentions the value it will be given. The "point" is the argument; point-free means argument-free. `extractLevel(line) = uppercase(trim(fourthField(line)))` is **pointed**: it names `line`. Its point-free twin says `extractLevel` *is* the chain `fourthField` then `trim` then `uppercase`, and says nothing about any line. The important thing is that this is a **rewrite of the text, not a different function**. Nothing new is computed and no stage is changed. The only thing that disappears is a name. ## The condition that lets the name go Look at what the parameter does in the body. The name is erasable when all three of these hold: 1. **It occurs exactly once.** One mention, nowhere else in the body. 2. **That occurrence is the argument the whole body is applied to.** Peel the calls from the outside in — `uppercase(...)`, then `trim(...)`, then `fourthField(...)` — and the parameter is what you reach at the bottom. 3. **Nothing else in the body depends on it.** No branch chooses a stage based on it, no second expression consults it. When those hold, the body is literally `chain(parameter)` for some chain you could name. Give the chain a name and the definition reads `extractLevel(line) = levelPipeline(line)` — a function that does nothing but forward its argument. Erasing that forwarding step is **eta-reduction**: `f(x) = g(x)` becomes `f = g`. Putting the parameter back is the inverse, **eta-expansion**, and it is how you get a pointed definition again when you want one. Notice what the condition is *not*. It is not about how many stages there are, and it is not about the stages sharing a type. It is about whether the argument is doing any work in the body beyond being passed on. ## What blocks the rewrite | Shape of the body | Why the name cannot simply go | What forcing it costs | |---|---|---| | The parameter is used twice | No single chain applies it once | A combinator that feeds one value to two stages and rejoins their results | | The parameter fills a stage's non-final argument slot | That stage needs its other arguments before it can become a one-argument step | Fixing the other arguments first so the stage takes only the value | | The body branches on the parameter | The choice is made from the value, not by a chain | A combinator that takes both branches as functions and picks between them | | The body does two unrelated things with it | There is more than one chain | A combinator that pairs the results and a stage that consumes the pair | Each row is still *expressible* point-free. The question is what you pay: in every row the deleted name is replaced by machinery that exists only to route a value, which is exactly where the style stops being free. ## What the rewrite changes, and what it does not - **The result does not change.** For a chain of pure stages, the same input yields the same output before and after. Referential transparency is what licenses the substitution in the first place. - **The arity is no longer visible in the definition text.** A reader who wants to know how many arguments `extractLevel` takes must look at the stages instead of at the definition. The information is still there; it just moved. - **When the composed value is built can differ.** In the pointed form the chain is assembled on every call; in the point-free form there is a single composed value built once at definition time. Evaluation models differ in whether that is observable at all, and for pure stages it is a cost question, never a result question. - **The intermediate values lose their names.** Between `fourthField` and `trim` there is a value that nobody calls anything. That is the real price of the style, and it is what a reviewer should weigh. ## Reading one back To read a point-free definition, supply an imaginary argument and walk the stage names in order, saying out loud what the value is at each boundary: a raw line, a field, a trimmed field, an upper-cased level. If you cannot say what the value between two stages is, the chain has outrun its own names — and that is a signal about the definition, not about your reading.

  • The body is uppercase(trim(fieldAt(line, 4))) — why can the name not be deleted as it stands?
    Because `line` is not the argument the chain is applied to; it is the first of two arguments to `fieldAt`, and the index comes after it. You have to turn `fieldAt` into a one-argument stage by fixing the index first. Once the stage takes only a line, the body becomes a single chain applied once to the parameter and the name can go.
  • Does putting the parameter back — eta-expansion — ever change what the function computes?
    For a chain of pure stages, no: the same argument produces the same result either way, which is why the rewrite is safe in both directions. What can differ is when the composed value is assembled and how often, and evaluation models differ on whether that is observable. It is a cost and timing question, not a result question.
  • A definition has exactly one parameter mention but wraps the chain in a branch on that value. Is it erasable?
    Not directly. The parameter is mentioned once in the branch test, but the branch consults it rather than forwarding it, so the body is not a single chain applied to the argument. Making it point-free means introducing a combinator that takes the test and both alternatives as functions — machinery that replaces one clear name with three moving parts.

saying these in an interview costs you the question

  • Thinks point-free means the function takes no arguments at all
  • Says a definition is erasable whenever all its stages are pure
  • Believes the stages must all share one type for the name to go
  • Deletes a parameter that appears twice and expects the chain to work
  • Treats the rewrite as an optimisation that makes the function faster
  • Claims a point-free definition can no longer be applied to a value
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 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

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