skip to content

Unification and Binding

Rego's = unifies, := assigns and == compares, and an unbound variable silently becomes a loop. Interviewers use it because reusing one variable in a body can narrow a rule to nothing.

on this pageshow

questions

4

In Rego, what is the difference between =, := and == in a rule body?

level: juniorimportance: must knowfreq 74%

answer

  1. three symbols, three different jobs
  2. only some of them create a binding
  3. one is declaration and is rule-scoped
  4. comparison needs both sides already bound
  5. unification binds in either direction

basics

~20 s

:= declares a local variable and assigns a value to it. == compares two values that are already bound and yields true or false. = unifies: it binds whichever side is still unbound, and compares when both sides are bound.

solid answer

~50 s

`:=` is assignment: it declares a new variable local to the rule, and re-declaring the same name in the same scope is a compile error. `==` is comparison: both operands must already be bound, it never introduces a binding, and it produces `true` or `false`. `=` is unification: it makes the two sides equal by binding variables on either side, so `x = input.spec.replicas` binds `x`, while `input.a = input.b` is just a comparison because nothing is free. In modern policy style you write `:=` for locals and `==` for tests, which makes it obvious to a reader which expressions create bindings; `=` still appears in rule heads such as `limit = 2 if { ... }` and where you genuinely want either side to bind. Getting this wrong shows up as a compile error rather than a wrong decision, which is the good case.

go deeper

for a junior

Be ready to state the three jobs in one breath: := declares and assigns, == compares two bound values, = unifies and can bind either side. Knowing that Rego has no reassignment is half the answer.

for a middle

Explain the mechanics: which operator introduces a binding, why an unbound variable next to == is a compile-time unsafe-variable error, and why := is scoped to the rule body rather than the package.

for a senior

Show the review judgment. Explain why a team standardises on := and == so the compiler catches mistakes, and why a stray = is the one error that compiles and quietly makes a check always succeed.

for a principal

Own the convention. Decide whether the policy repo enforces assignment-and-comparison style in review or in a lint step, and be able to say what class of silent gate failure that convention is buying protection from.

Rego is a declarative language, so the three symbols that look like variants of "equals" are actually three different operations. Interviewers ask this first because every later confusion about iteration and undefined results starts here. ## `:=` — assignment (declaration) `x := input.spec.template.spec.containers` declares `x` as a **local variable of the enclosing rule** and gives it a value. Two properties matter: - **It declares.** The name must not already be declared in the same scope. Writing `x := 1` and later `x := 2` in one rule body is a compile error, not a reassignment — there are no mutable variables in Rego. If you want a second value, use a second name. - **It is local.** The binding is visible for the rest of that rule body (and to the rule head), not to other rules in the package. The right-hand side may itself iterate: `c := input.spec.template.spec.containers[_]` is legal and binds `c` once per element, because the wildcard on the right supplies the bindings. ## `==` — comparison `count(names) == 0` evaluates both sides and returns a boolean. It **never binds anything**. That has a direct consequence: if either side contains a variable that no other expression in the body binds, the rule does not compile and OPA reports that the variable is unsafe. `==` is also the reason a body can fail without being an error — an expression that evaluates to `false` makes the body fail, and the rule produces no result. ## `=` — unification Unification is the Prolog-style operation: make the two sides equal, binding free variables on **either** side to do it. Three cases: - Both sides ground: `1 = 2` simply fails; `1 = 1` succeeds. Here `=` behaves like `==`. - One side free: `x = input.spec.replicas` binds `x`. Direction does not matter — `input.spec.replicas = x` binds `x` too. - Both sides contain structure: `{"name": n} = c` binds `n` to the container's name by matching shape, which is how you destructure a document. Because `=` can silently create a binding where a reader expected a test, style guidance is to reserve it for the places it is genuinely needed and use `:=` / `==` everywhere else. The one place you still see `=` constantly is the head of a complete rule: `max_replicas = 10 if { ... }` assigns the rule's value. ## Why this matters for a gate A rule that checks resource limits on a workload rendered out of a chart might read: - `c := input.spec.template.spec.containers[_]` — binds each container in turn (assignment plus iteration). - `c.resources.limits.cpu == "0"` — a test on an already-bound value. - `some name; name = c.name` — unification, and the same thing `:=` would do more clearly. The failure modes are asymmetric and worth knowing: | Mistake | What happens | | --- | --- | | Using `==` with a variable nothing else binds | compile error: the variable is unsafe | | Using `:=` twice for one name | compile error: re-declaration | | Using `=` where you meant a test | compiles, but quietly binds and the expression always succeeds | The last row is the dangerous one, because a gate that always succeeds is a gate that never denies. That is why reviewers on a policy repo push hard for `:=` and `==`: the compiler catches the other two mistakes for you, and the one it cannot catch is the one you removed from the codebase by convention. ## A note on scope Variables declared with `:=` (and with `some`) are scoped to the rule body, and variables introduced inside a comprehension are scoped to the comprehension. Nothing leaks between rules. A value shared across rules is expressed as another rule, not as a variable — which is the same reason Rego has no assignment statement in the imperative sense.

  • What happens if the same name is assigned with := twice in one rule body?
    It is a compile error. `:=` declares the variable, and Rego has no reassignment — a name is bound once per scope. Introduce a second name, or restructure so the value is computed in one expression. The same error appears if you shadow a name already declared by `some` in that body.
  • Why does a body containing only count(x) == 3 fail to compile when x appears nowhere else?
    Because `==` compares but never binds, `x` is left free, and OPA rejects the rule with an unsafe-variable error. Something in the body must bind `x` first — an assignment, a reference that iterates, or a `some` declaration with a collection to draw from.
  • When do you genuinely need = rather than := or ==?
    In the head of a complete rule, where `value = expr if { ... }` defines what the rule evaluates to, and when you want to destructure by matching a pattern against a document so the variables inside the pattern bind. Everywhere else `:=` and `==` say the same thing with less ambiguity.

saying these in an interview costs you the question

  • Says = and == are interchangeable anywhere in a rule
  • Thinks := reassigns a variable later in the same body
  • Expects == to bind an unbound variable
  • Calls := a mutation of shared state across rules
  • Believes = only ever compares and never binds

context

open as a page

In Rego, what does the wildcard _ do in a reference like input.spec.containers[_].name?

level: middleimportance: must knowfreq 60%

basics

~20 s

The underscore is an anonymous variable, so the reference iterates: OPA tries every element of the array and the expression succeeds if at least one element makes it hold. Each underscore is a separate variable whose value you cannot refer to later.

open as a page

In Rego, how do you require that every container in a list has CPU and memory limits?

level: middleimportance: should knowfreq 48%

basics

~20 s

Use the every keyword: every c in the container list, the body asserting the limits exist. The alternative is to find a counterexample with iteration and negate it. Plain iteration cannot express this, because an unbound variable means at least one element.

open as a page

Your Rego deny rule names only the first container missing resource limits — how do you fix it?

level: seniorimportance: should knowfreq 36%

basics

~20 s

The comprehension collapses every offender into one array, and indexing it at [0] picks a single name. Move the iteration into the rule body so each binding produces its own set entry, and iterate the container field names so init and ephemeral containers are covered too.

open as a page