skip to content

How should a three-argument permission check order its parameters so the specialised checkers you want are cheap to take?

level: middleimportance: should knowfreq 48%

answer

  1. specialisation consumes a prefix
  2. slowest-changing argument first
  3. the per-call datum goes last
  4. one ordering privileges one narrowing
  5. wrong order means a reordering wrapper

basics

~20 s

Put the arguments you specialise on - the slow-changing ones such as role and resource - in the leading positions and the per-call argument last, because the usual specialisation helpers and every curried chain consume the parameter list from the left.

solid answer

~50 s

Specialisation eats a **prefix**. Applying a curried check one rung at a time, or fixing arguments with a helper, both start at the leftmost parameter, so the arguments you want to bake in have to sit at the front. Order them by how often each one changes: the role and the resource change per module or per wiring, the action changes per request, so `check(role, resource, action)` lets you take `check(role)(resource)` and hold a checker that asks only for the action. Flip it to put the action first and that same specialisation needs a hand-written wrapper that reorders the call. One ordering can only make one family of specialisations free, so choose it for the narrowing you actually take most often, and give the other one an explicit named entry point rather than pretending both are cheap.

code

pseudocode · 11 lines
pseudocode
// slow-changing first, per-call argument last
function check(role, resource, action)

onDocs   = curried(adminRole)(documentResource)   // needs only the action
decision = onDocs(deleteAction)

// action-first ordering: the same narrowing needs a wrapper
function checkActionFirst(action, resource, role)

function onDocsActionFirst(action)
    return checkActionFirst(action, documentResource, adminRole)

go deeper

for a junior

Recall the rule of thumb: the arguments that stay the same across many calls go at the front, and the one that changes on every call goes at the end.

for a middle

Explain why - both currying and the usual fixing helpers consume a prefix - and show what fixing a trailing argument costs: a named wrapper that reorders the call.

for a senior

Treat the order as part of the published contract. Choose it for the narrowing your callers actually take, and notice when a growing pile of reordering wrappers says you chose wrong.

for a principal

Decide whether to publish one general function plus specialisation or a small family of named entry points, knowing the first centralises logic and the second reads better at every call site.

This is an API-design question hiding inside a functional-programming one. The parameter order of a function decides which specialisations of it are free and which cost a wrapper, and once the function is published the order is expensive to change. ## Specialisation consumes a prefix A permission check takes a `role`, a `resource` and an `action`. Both ways of specialising it work from the left: - A **curried** chain is literally ordered: the first rung takes the first parameter, and there is no way to reach the third without passing through the first two. - A **partial application** helper conventionally fixes the first `k` arguments and leaves the remaining `n - k` open. So a prefix of the parameter list is what you can bake in cheaply. Some settings do offer a way to fix a non-leading position, which loosens the mechanics; it does not loosen the readability cost, because the resulting call site no longer reads left to right. ## Order by rate of change The practical rule is to sort parameters by how often each one varies, slowest first: - **Configuration and policy** - which role, which tenant, which policy table. Fixed once at wiring time. - **Subject of the work** - which resource type. Fixed per module, or per handler family. - **The per-call datum** - which action this request attempts. Varies every single call. With `check(role, resource, action)`, a module that only ever guards documents for administrators takes `check(adminRole)(documentResource)` once and passes around a checker of one argument. The general check remains the only place the decision logic lives. | Ordering | Specialising by role and resource | Specialising by action | |---|---|---| | `check(role, resource, action)` | free: fix a prefix of one or two | needs a reordering wrapper | | `check(action, resource, role)` | needs a reordering wrapper | free: fix the leading action | | `check(resource, role, action)` | free for resource, wrapper for role alone | needs a reordering wrapper | The table makes the real constraint visible: **no single ordering makes every specialisation free.** Ordering is a choice about which one you privilege. ## When the argument you need to fix is not leading Three honest options, in the order most teams should consider them: 1. **Change the parameter order** if the function is yours and the specialisation you keep reaching for is the one the order punishes. This is the cheapest fix before publication and the most expensive after. 2. **Write the wrapper.** A small named function that accepts the still-open arguments and calls through with the fixed one in place is clear, greppable and costs a few lines. It is a partial application done by hand, and it works no matter where the fixed argument sits. 3. **Publish a second entry point** when two different narrowings are both common. Two named functions that both delegate to one general check are usually kinder to readers than one function plus a pile of ad-hoc reorderings at call sites. What you should not do is reorder arguments at random call sites with anonymous wrappers. That is the state in which nobody can tell which arguments a floating checker has already absorbed. ## Why this is a design decision, not a style preference - The order is part of the published signature: changing it later breaks every caller. - The order tells a reader which narrowing the author expected, so a sensible order is documentation. - The order determines whether a specialised checker can be created once at start-up or has to be assembled at each call site. - Getting it wrong is not fatal - a wrapper always rescues you - but the wrappers accumulate, and each one is a place where the argument order can be transposed silently. ## The one-line version Parameters you will fix go first; the argument that changes on every call goes last. If you cannot decide which will be fixed, that is a sign the function does two jobs and the split is the real fix.

  • Two teams want opposite specialisations from the same check - one per role, one per action. What do you ship?
    One general check with the order that serves the commoner narrowing, plus a named second entry point for the other that delegates to it. Trying to make both free leads to argument juggling at call sites; two named functions over one implementation keeps the decision logic single and both call sites readable.
  • Does this advice depend on the check being curried?
    No. A curried chain forces left-to-right consumption, and the common specialisation helpers fix a leading run of arguments, so the prefix rule applies either way. Where a mechanism exists to fix a non-leading argument, the mechanics relax but the call site stops reading in parameter order.
  • What if the argument you want to fix is the one that changes most often?
    Then you are not specialising, you are just calling the function. Fixing a per-request value produces a checker with a lifetime of one request, which buys nothing over passing the argument through, and it risks that checker outliving the request it was built for.

saying these in an interview costs you the question

  • Thinks any parameter is as cheap to fix as the first.
  • Puts the per-request argument first and the configuration last.
  • Says parameter order is only a matter of taste.
  • Assumes one ordering can make every specialisation free.
  • Claims order matters only once the function is curried.