skip to content

First-Class Functions

Treating a function as a value you can store, pass and return - the property everything else here is built on. Interviewers check you separate it from higher-order functions, its consequence.

on this pageshow

explore

questions

17

In an editor that keeps each shortcut's behaviour in a map entry, what does that require of functions?

level: juniorimportance: must knowfreq 66%

answer

  1. behaviour is data here
  2. anywhere a number can go
  3. variable, field, map entry, argument
  4. no central conditional over codes
  5. the table itself dispatches

basics

~20 s

Functions have to be ordinary values: storable in a variable, a field or a map entry, and passable and returnable like a number. The table then holds behaviour itself rather than a code that some conditional must interpret.

solid answer

~40 s

A function value is a value in the full sense - it can be bound to a variable, held in a field, put in a map entry, handed to another routine as an argument, and handed back as a result. That is what lets the shortcut table *be* the dispatcher: the entry for a key carries the behaviour, so adding a shortcut is one new entry and nothing else changes. Without that property you store a code or a name in the table and keep a central conditional that turns each code into a call, so every new action edits a second place. Both a named declaration and an inline literal produce the same kind of value, and the entry cannot tell them apart.

code

pseudocode · 10 lines
pseudocode
table = empty map
table["ctrl+s"] = save                          // a named declaration
table["ctrl+q"] = function() prompt("quit?") end // an inline literal

function lookup(t, k)
    return t[k]        // returns a function value, like returning a number
end

action = lookup(table, "ctrl+s")
if action is missing then report unbound key else action()

go deeper

for a junior

Say plainly that a function can live in a variable, a field or a map entry and be handed around like a number, then show the entry being looked up and applied.

for a middle

Explain what the alternative costs: an action code per entry plus one conditional that every new action must edit, against a table whose entries carry the behaviour directly.

for a senior

Treat the table as an extension point and talk about duplicate keys, registration order, and what a missing entry should do at press time rather than at registration.

for a principal

Weigh a bare function value per entry against a record with a behaviour field, once entries also need a label, an enabled test and an undo step - that is a design decision, not a syntax preference.

## What first-class actually asks of a language Calling functions **first-class** is a claim about where a function is allowed to appear. A value is first-class when the language lets it go everywhere its ordinary values go. For functions that means, concretely: - bound to a local variable or a constant; - stored in a field of a record or an object; - placed in a collection - an element of a list, a value in a map, a member of a set; - passed as an argument to another routine; - returned as the result of a routine; - created anonymously at the point of use, without a declaration statement. Nothing in that list is exotic; it is the same list you would write for an integer. The interview-worthy part is what the property enables, not the definition itself. ## The table with behaviour versus the table with codes An editor needs each keyboard shortcut to trigger some behaviour. There are two shapes for that. | design | what the entry holds | where dispatch lives | cost of a new action | |---|---|---|---| | behaviour as data | a function value | in the table itself | one new entry | | codes plus a conditional | an action code or a name | in one central conditional | a new entry **and** a new branch | The second shape is what you are forced into when behaviour is not a value. It works, and plenty of systems ship it, but the dispatch decision is split across two places that must stay in step. The first shape collapses them: the lookup **is** the decision, because what comes back is already the thing to run. ## Where such a value may live The same value can be reached three ways in one program, and it is worth being able to show all three: 1. A **variable** holds it while the code decides what to register. 2. A **field** on a record holds it alongside a label and an enabled flag, so a command carries its behaviour with its metadata. 3. A **map entry** holds it under the key that should trigger it. In all three the value is the same kind of thing. Passing it into a helper that registers it, or returning it from a lookup, changes nothing about it - a function value survives being moved around exactly as a number does. ## Passing and returning the value A routine that takes the table and a key and returns the stored entry is returning a function value, and the caller then applies it. Notice how little ceremony that needs: no wrapper type, no interpretation step, no agreement about what the code `7` means. The behaviour travelled as itself. That is also why a missing entry has to be handled explicitly - the absence of behaviour is a real case, and it is one the code-plus-conditional shape usually handles with a default branch instead. ## What the design buys and what it costs The gains are concrete: - New actions are additive; no existing site enumerates the set of actions. - The table reads as a specification of the key map, one line per binding. - The behaviour can be applied by a test directly, without simulating a key press. The costs are real too: - A table of function values is harder to print, serialise or show in a settings screen than a table of codes, because a function value usually carries no useful description of itself. - If entries later need a label, an undo step or an enabled test, the entry has to become a record with a behaviour field rather than a bare function value - a small refactor, but one worth anticipating. - Some languages will happily accept a value of the wrong kind in the entry, so the mistake of storing a result rather than a function is caught late. ## What this property is not Being able to hold and move a function value is the base property. It is not the same as writing routines that consume or produce other functions as a design technique, it is not the capture of surrounding variables by a function written inside another, and it is not the type notation used to describe the entry. Those are separate subjects that all rest on this one. The claim here is narrow and worth stating precisely: behaviour can be a value, so behaviour can be data, so a lookup table can dispatch.

  • What does the same shortcut table look like in a language where behaviour is not a value?
    Entries hold action codes or names, and a central conditional maps each code to a call. The table then carries only half the dispatch decision, and every new action edits that conditional as well as the table.
  • What does the dispatcher gain when a new shortcut is added?
    Nothing changes in it at all. The entry carries the behaviour, so registration is purely additive and no site enumerates the set of actions. Only the missing-entry case still lives in the dispatcher.

saying these in an interview costs you the question

  • Says only dynamically typed languages can put a function in a map
  • Thinks storing behaviour always needs a one-method wrapper object
  • Confuses storing the function with storing its name as text
  • Believes a stored function keeps running in the background
  • Thinks the whole table must be rebuilt to add one action
open as a page

A shortcut table is filled with table[key] = save(), and the document saves at startup - what was stored?

level: juniorimportance: must knowfreq 74%

basics

~20 s

The entry stored the result of calling save, not the function itself: the parentheses invoked it while the table was being built. Writing table[key] = save stores the function value and leaves the call for key-press time.

open as a page

A chat client hands a send routine a function to run on acknowledgement — what control has the caller given up?

level: juniorimportance: must knowfreq 70%

basics

~20 s

The caller gives up control of invocation: the send routine now decides whether the acknowledgement handler runs at all, when it runs, how many times, with what arguments, and on which execution context. That reversal is inversion of control.

open as a page

A renderer declares its cell-formatter hook as one value in, one display string out — what does that declaration constrain?

level: juniorimportance: must knowfreq 68%

basics

~20 s

A function type fixes arity plus what stands in each parameter position and in the result position. A supplied formatter must accept the one argument the renderer will pass and hand back something the renderer can print.

open as a page

What must a callback-shaped API document about the handler it accepts, beyond that handler's parameters?

level: middleimportance: must knowfreq 56%

basics

~20 s

The invocation contract: how many times the handler may run, whether it can run before the registering call returns, on which execution context, how failures are delivered to it, and what the routine does if the handler itself throws.

open as a page

Why does a formatter that accepts any value still fit an amount-in, text-out slot, while one returning any value does not?

level: middleimportance: must knowfreq 58%

basics

~10 s

The caller only ever passes an amount and always prints what comes back. So a formatter may accept wider than promised but must return no wider: looser in parameter position, tighter in result position.

open as a page

What does one beta-reduction step replace when you reduce (λx. λy. x) a b by hand?

level: juniorimportance: should knowfreq 40%

basics

~20 s

A beta-reduction step substitutes the argument for every free occurrence of the parameter in the function body, then removes the binder. Two such steps take (λx. λy. x) a b to a, discarding the second argument.

open as a page

When registering an editor shortcut, when is a named function declaration better than an inline literal?

level: middleimportance: should knowfreq 46%

basics

~20 s

Prefer a named declaration when something else needs the same behaviour - a second binding, a direct test, or a later removal. An inline literal is right when the body exists only to fit the registration site's shape.

open as a page

In a chat client, three acknowledgement handlers nested inside one another — what becomes hard to read and change?

level: middleimportance: should knowfreq 50%

basics

~20 s

Sequence stops being expressed by order of lines and becomes indentation. Each level needs its own failure path, no statement means "after all three steps", early return no longer abandons the sequence, and the step count is fixed at authoring time.

open as a page

The renderer calls every formatter with two arguments, the value and its row — what does supplying a one-parameter function violate?

level: middleimportance: should knowfreq 46%

basics

~20 s

Arity is part of a function's type, not a detail of the body. A one-parameter function therefore has a different type from a two-argument slot, and needs an adapter that takes both arguments and forwards one.

open as a page

Forty columns each declare the same one-argument-to-text formatter shape inline — what does giving that shape one name buy you?

level: middleimportance: should knowfreq 42%

basics

~20 s

Naming a recurring function type gives the contract one place to be read and changed, and reviewers a word for it. Where types match by shape it changes no compatibility; where they match by name, the name becomes the type.

open as a page

With only abstraction and application available, how can a term encode a pair and recover each component?

level: middleimportance: should knowfreq 26%

basics

~20 s

A pair becomes a function that carries both components and hands them to a selector the caller supplies. Apply it to a selector returning its first argument to get one component, and to a selector returning its second for the other.

open as a page

Reducing (λx. λy. x) y, why does naive substitution produce the identity function instead of the correct term?

level: middleimportance: should knowfreq 34%

basics

~20 s

The argument is the free variable y, and substituting it blindly drops it under an inner binder that already uses that name, so it is captured. Renaming the bound name first gives λz. y, a constant function.

open as a page

An editor unbinds a shortcut by the function value it was registered with - why can an inline literal break that?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Unbinding matches the stored value, and evaluating an inline literal generally yields a fresh, distinct value. Registering with one literal and unbinding with a second, identical-looking literal compares two different values, so the binding stays.

open as a page

A chat client queues one zero-argument function per unsent message — what does holding work as function values cost the queue?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Opacity. A queued function value can only be run, never read, so the queue cannot deduplicate, prioritise, report on, persist or retry-with-different-arguments work it cannot inspect. Uniformity is what it buys in exchange.

open as a page

Forty teams implement your published formatter slot — which edits to its declared type leave every existing implementation valid?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Two edits are safe: narrowing the parameter type the slot promises to pass, and widening the result type it will accept back. Both ask implementers for less. Widening the parameter, narrowing the result or changing arity moves work onto all forty teams.

open as a page

Nothing in the pure lambda calculus can refer to itself by name, so how does recursion arise?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Through a fixed-point combinator. You write the body as a function whose first parameter stands for the recursive call, then apply a combinator that keeps handing that body another copy of itself, so the call site is supplied rather than named.

open as a page