0

Lambda vs closure vs arrow function — the exact differences

Intermediate5 min read·eng-12-012
interviewcompare

Concept

Lambda vs closure vs arrow function — three terms for anonymous functions in PHP, with precise differences.

Anonymous function (legacy term, pre-PHP 5.3): A function without a name. In PHP, function() { } is the syntax. The Closure class instance is what PHP creates.

Closure (PHP term): PHP's Closure class is what anonymous functions ARE. When you write $fn = function() { }, $fn is a Closure instance. Closures can capture variables from the outer scope using use ($var).

  • use ($var): Capture by value (copy at the time of closure creation).
  • use (&$var): Capture by reference (shares the variable).
  • Inside a closure, $this is NOT automatically available unless the closure is bound to an object.

Lambda (computer science term): A function value — a function that can be stored in a variable and passed around. In PHP, all anonymous functions are lambdas in this sense. The terms are often used interchangeably.

Arrow function (PHP 7.4+, fn() =>):

  • Single expression, no {} block.
  • Automatically captures variables from the outer scope (no use keyword needed).
  • Cannot have a return type declaration (though type inference works).
  • Cannot span multiple lines (except with complex expressions).
  • Returns the expression value implicitly (no return keyword).
  • Cannot capture by reference (no use (&$var) equivalent).

When to use which:

  • Multi-line logic → function() { ... } (regular closure).
  • Single expression with outer variables → fn($x) => expression (arrow function — cleaner).
  • Named, reusable function → regular function or method.

Code Example

php
<?php
// Regular closure — multi-line, explicit capture
$multiplier = 3;
$multiply   = function(int $n) use ($multiplier): int {
    // $multiplier captured by value at closure creation time
    return $n * $multiplier;
};
echo $multiply(5); // 15

$multiplier = 10;        // changing outer variable...
echo $multiply(5);       // still 15 — captured by VALUE (copy)

// Capture by reference — shares the variable
$count   = 0;
$counter = function() use (&$count): void { $count++; };
$counter(); $counter(); $counter();
echo $count; // 3 — modified via reference

// Arrow function — automatic capture, single expression
$prefix = 'Hello';
$greet  = fn(string $name): string => "{$prefix}, {$name}!"; // no 'use' needed
echo $greet('Alice'); // "Hello, Alice!"

$prefix = 'Goodbye';
echo $greet('Alice'); // "Goodbye, Alice!" — arrow fn captures the variable itself (NOT a copy!)
// Wait — arrow functions capture by VALUE too, but the value is re-evaluated each call? NO.
// Arrow functions capture at definition time, by value. Let me clarify:
// Actually arrow functions capture by VALUE at DEFINITION time (same as 'use ($var)')
// $prefix was 'Hello' when $greet was defined, so it's always 'Hello'

// More accurate example:
$tax      = 0.1;
$prices   = [100, 200, 300];
$withTax  = array_map(fn($p) => $p * (1 + $tax), $prices); // [110, 220, 330]

// vs regular closure:
$withTax2 = array_map(function($p) use ($tax) { return $p * (1 + $tax); }, $prices);

// Closure bound to an object
class Counter
{
    private int $count = 0;
    public function getIncrementer(): \Closure
    {
        return function() { $this->count++; return $this->count; }; // $this available here
    }
}
$counter = new Counter();
$inc     = $counter->getIncrementer();
echo $inc(); // 1
echo $inc(); // 2

// Closure::bind() — bind a closure to a different object
$fn   = Closure::bind(function() { return $this->count; }, $counter, Counter::class);
echo $fn(); // 2

// Practical: array functions with arrow functions (cleaner)
$users    = [['name' => 'Alice', 'age' => 25], ['name' => 'Bob', 'age' => 30]];
$names    = array_map(fn($u) => $u['name'], $users);    // ['Alice', 'Bob']
$adults   = array_filter($users, fn($u) => $u['age'] >= 18);
$youngest = array_reduce($users, fn($min, $u) => $u['age'] < ($min['age'] ?? PHP_INT_MAX) ? $u : $min);