🐘 PHP

الدوال السهمية والأسلوب الوظيفي في PHP — دليل شامل

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы пришли в кафе быстрого питания. Вы подходите к стойке и говорите: «Сделайте мне бургер». Повар берёт котлету, булочку, овощи и заворачивает всё в бумагу. В этой ситуации повар — это функция, которая принимает ингредиенты и возвращает готовое блюдо.

А теперь представьте, что вы хотите заказать не просто бургер, а способ приготовления. Вы говорите: «Сделайте бургер, но котлету прожарьте сильно, а лук положите кольцами». Вы передаёте повару не просто список продуктов, а инструкцию, как именно их обработать.

В программировании лямбда-функция — это именно такая инструкция. Это функция, у которой нет имени. Вы создаёте её «на лету», прямо в том месте, где она нужна, и передаёте её другой функции как инструкцию к действию.

Простыми словами: Лямбда — это функция-однодневка. Вы её написали, использовали один раз, и она исчезла. Не нужно придумывать ей имя, не нужно объявлять её заранее.

Функциональный стиль — это подход, при котором мы активно используем такие функции-инструкции. Вместо того чтобы писать длинные циклы for или foreach, мы говорим: «Возьми этот массив и примени к каждому элементу вот такое правило». Код становится короче, понятнее и меньше подвержен ошибкам.

Зачем это нужно? Представьте, что у вас есть список чисел, и вам нужно:

  • Оставить только чётные
  • Умножить каждое на 2
  • Отсортировать по убыванию

Без лямбд вам пришлось бы писать три отдельных цикла или создавать три отдельные именованные функции. С лямбдами это делается в три строчки, и каждая строчка читается как предложение на английском: array_filter, array_map, usort.

Теория: как это работает изнутри

Чтобы понять, как работает лямбда-функция, нужно заглянуть «под капот» PHP. Когда вы пишете обычную функцию:

function add($a, $b) {
    return $a + $b;
}

PHP создаёт в памяти запись: «Есть функция с именем add, она принимает два аргумента, делает сложение, возвращает результат». Эта запись хранится в глобальной таблице функций. Вы можете вызвать её по имени в любом месте программы.

Когда вы пишете лямбду:

$add = function($a, $b) {
    return $a + $b;
};

Происходит то же самое, но с одним отличием: функция не регистрируется в глобальной таблице. Вместо этого PHP создаёт объект класса Closure (замыкание). Этот объект хранит:

  • Код функции (что нужно сделать)
  • Список аргументов
  • Список захваченных переменных из внешней области видимости (если есть)
  • Ссылку на область видимости, в которой функция была создана

Ключевое отличие: Обычная функция — это статическая запись в таблице. Лямбда — это объект, который можно передавать, сохранять в переменные, возвращать из других функций.

Когда вы передаёте лямбду в другую функцию, например, в array_map, происходит следующее:

  1. PHP берёт объект Closure
  2. Передаёт его в целевую функцию как аргумент
  3. Целевая функция вызывает этот объект как функцию (используя оператор ())
  4. Объект Closure выполняет код, который вы написали
  5. Возвращает результат
Важно: Лямбда-функция не «знает», где она будет вызвана. Она «помнит» только то, что вы в неё передали и какие переменные захватили из внешнего кода.

Замыкание (closure) — это лямбда, которая «захватывает» переменные из внешней области видимости. Например:

$factor = 3;
$multiply = function($x) use ($factor) {
    return $x * $factor;
};

Ключевое слово use говорит PHP: «Возьми переменную $factor из внешнего кода и сделай её доступной внутри лямбды». Без use лямбда не увидит внешние переменные — это сделано специально, чтобы избежать случайных ошибок.

Переменные захватываются по значению (копируются), если не указать & перед именем. Если написать use (&$factor), то будет захвачена ссылка, и изменения внутри лямбды повлияют на оригинальную переменную.

Синтаксис в PHP: все формы и варианты

PHP предлагает несколько способов создания лямбда-функций. Рассмотрим каждый.

1. Анонимная функция (базовый синтаксис)

// Самая простая форма: function(аргументы) { тело }
$greet = function($name) {
    return "Привет, $name!";
};

echo $greet("Анна"); // Выведет: Привет, Анна!

Обратите внимание на точку с запятой после закрывающей фигурной скобки. Это обязательно, потому что мы присваиваем значение переменной.

2. Стрелочная функция (PHP 7.4+)

// Короткий синтаксис: fn(аргументы) => выражение
$greet = fn($name) => "Привет, $name!";

echo $greet("Анна"); // Выведет: Привет, Анна!

Стрелочная функция — это сокращённая запись. Она всегда возвращает результат выражения (ключевое слово return не нужно). Она автоматически захватывает все внешние переменные по значению (как если бы вы написали use для каждой).

Ограничение: Стрелочная функция может содержать только одно выражение. Нельзя написать несколько операторов или использовать циклы. Для сложной логики используйте обычную анонимную функцию.

3. Лямбда с захватом переменных (use)

$prefix = "Уважаемый(ая) ";
$greet = function($name) use ($prefix) {
    return $prefix . $name;
};

echo $greet("Иван"); // Выведет: Уважаемый(ая) Иван

Можно захватывать несколько переменных через запятую: use ($a, $b, $c).

4. Лямбда с захватом по ссылке

$counter = 0;
$increment = function() use (&$counter) {
    $counter++;
};

$increment();
$increment();
echo $counter; // Выведет: 2

Без & переменная $counter внутри лямбды была бы копией, и внешняя переменная не изменилась бы.

5. Лямбда как callback для встроенных функций

$numbers = [1, 2, 3, 4, 5];

// Используем лямбду для фильтрации
$even = array_filter($numbers, function($n) {
    return $n % 2 == 0;
});

print_r($even); // [2, 4]

Это самый частый сценарий использования. Лямбда передаётся как аргумент в другую функцию.

6. Лямбда, возвращаемая из функции

function createMultiplier($factor) {
    return function($x) use ($factor) {
        return $x * $factor;
    };
}

$double = createMultiplier(2);
$triple = createMultiplier(3);

echo $double(5); // 10
echo $triple(5); // 15

Это пример фабрики функций. Мы создаём функцию, которая возвращает другую функцию с уже настроенными параметрами.

7. Самовызывающаяся лямбда (IIFE)

$result = (function($a, $b) {
    return $a + $b;
})(10, 20);

echo $result; // 30

Лямбда определяется и сразу же вызывается. Это полезно, когда нужно создать временную область видимости для переменных.

Примеры кода — от простого к сложному

Пример 1: Простейшая лямбда в переменной

// Создаём лямбду, которая возводит число в квадрат
$square = function($n) {
    return $n * $n;
};

// Вызываем её как обычную функцию
echo $square(4); // Выведет: 16

// Объяснение:
// 1. Создаём анонимную функцию и присваиваем её переменной $square
// 2. Функция принимает один аргумент $n
// 3. Возвращает квадрат числа
// 4. Вызываем функцию через переменную, передавая число 4

Пример 2: Фильтрация массива с array_filter

// У нас есть массив чисел
$numbers = [10, 25, 3, 47, 18, 60, 5];

// Хотим оставить только числа больше 20
$filtered = array_filter($numbers, function($value) {
    return $value > 20;
});

// Выводим результат
print_r($filtered);
// Результат: [1 => 25, 3 => 47, 5 => 60]
// Обратите внимание: ключи сохраняются!

// Объяснение:
// 1. array_filter проходит по каждому элементу массива
// 2. Для каждого элемента вызывает нашу лямбду
// 3. Если лямбда вернула true — элемент остаётся
// 4. Если false — элемент удаляется
// 5. Лямбда проверяет: значение больше 20?

Пример 3: Преобразование массива с array_map

// У нас есть массив имён
$names = ["анна", "иван", "мария", "петр"];

// Хотим сделать первую букву заглавной
$capitalized = array_map(function($name) {
    return ucfirst($name);
}, $names);

print_r($capitalized);
// Результат: ["Анна", "Иван", "Мария", "Петр"]

// Объяснение:
// 1. array_map применяет функцию к каждому элементу
// 2. Создаёт новый массив с результатами
// 3. Исходный массив не изменяется
// 4. ucfirst — встроенная функция PHP, делает первую букву заглавной

Пример 4: Сортировка с пользовательской логикой

// У нас есть массив людей с возрастом
$people = [
    ["name" => "Анна", "age" => 25],
    ["name" => "Иван", "age" => 30],
    ["name" => "Мария", "age" => 20],
    ["name" => "Петр", "age" => 35],
];

// Сортируем по возрасту (по убыванию)
usort($people, function($a, $b) {
    return $b['age'] - $a['age'];
});

print_r($people);
// Результат: Петр (35), Иван (30), Анна (25), Мария (20)

// Объяснение:
// 1. usort — сортировка с пользовательской функцией сравнения
// 2. Функция должна вернуть:
//    - отрицательное число, если $a < $b
//    - 0, если равны
//    - положительное число, если $a > $b
// 3. Мы вычитаем age $b из age $a, чтобы получить убывание
// 4. Если бы вычитали $a - $b, получили бы возрастание

Пример 5: Замыкание с аккумулятором

// Создаём функцию, которая возвращает счётчик
function createCounter() {
    $count = 0;
    return function() use (&$count) {
        $count++;
        return $count;
    };
}

// Создаём два независимых счётчика
$counter1 = createCounter();
$counter2 = createCounter();

echo $counter1(); // 1
echo $counter1(); // 2
echo $counter2(); // 1 (независимый счётчик!)
echo $counter1(); // 3
echo $counter2(); // 2

// Объяснение:
// 1. createCounter создаёт локальную переменную $count
// 2. Возвращает лямбду, которая захватывает $count по ссылке (&)
// 3. Каждый вызов createCounter создаёт новую переменную $count
// 4. Счётчики работают независимо, потому что у каждого своё замыкание
// 5. Без & переменная не изменялась бы между вызовами

Пример 6: Стрелочные функции для краткости

// Массив чисел
$numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Используем стрелочные функции (PHP 7.4+)
$even = array_filter($numbers, fn($n) => $n % 2 == 0);
$squared = array_map(fn($n) => $n * $n, $even);

print_r($squared);
// Результат: [4, 16, 36, 64, 100]

// Объяснение:
// 1. fn($n) => выражение — это стрелочная функция
// 2. Не нужно писать function, return и фигурные скобки
// 3. Результат выражения автоматически возвращается
// 4. Внешние переменные захватываются автоматически (не нужно use)
// 5. Но захват только по значению, ссылку передать нельзя

Пример 7: Композиция функций

// Создаём "строитель" цепочки обработки
function createPipeline(...$functions) {
    return function($initial) use ($functions) {
        $result = $initial;
        foreach ($functions as $func) {
            $result = $func($result);
        }
        return $result;
    };
}

// Создаём отдельные шаги обработки
$trim = fn($s) => trim($s);
$lower = fn($s) => strtolower($s);
$removeSpaces = fn($s) => str_replace(' ', '_', $s);

// Собираем конвейер
$process = createPipeline($trim, $lower, $removeSpaces);

// Применяем
echo $process("  Hello World  "); // hello_world

// Объяснение:
// 1. createPipeline принимает любое количество функций
// 2. Возвращает лямбду, которая применяет их по очереди
// 3. Результат каждой функции передаётся в следующую
// 4. Это называется "композиция" или "конвейер" функций
// 5. Очень удобно для последовательной обработки данных

Сравнение с Python: чем похоже и чем отличается

Характеристика PHP Python
Базовый синтаксис function($x) { return $x * 2; } lambda x: x * 2
Короткий синтаксис fn($x) => $x * 2 (PHP 7.4+) lambda x: x * 2 (всегда)
Захват переменных Явный через use Автоматический (лексическое замыкание)
Многострочность Да (обычная анонимная функция) Нет (только одно выражение)
Типизация Можно указать типы аргументов и возврата Динамическая, но можно аннотировать
Использование В основном как callback для встроенных функций Везде: map, filter, sorted, как аргументы
Объект функции Экземпляр Closure Экземпляр function
Производительность Немного медленнее именованных функций Немного медленнее именованных функций

Главное отличие: В Python лямбды — это единственный способ создать анонимную функцию, и они всегда однострочные. В PHP у вас есть выбор: короткая стрелочная функция для простых случаев и полноценная анонимная функция для сложной логики.

Второе отличие: Захват переменных. В Python лямбда автоматически видит все переменные из внешней области видимости. В PHP нужно явно указывать use. Это сделано для безопасности — вы случайно не захватите лишнюю переменную и не создадите трудноуловимую ошибку.

Третье отличие: В Python лямбды используются гораздо чаще и естественнее. В PHP функциональный стиль менее распространён, но становится популярнее с каждой новой версией.

Типичные ошибки и подводные камни

Ошибка 1: Забыли точку с запятой

// НЕПРАВИЛЬНО:
$func = function($x) {
    return $x * 2;
} // Ошибка! Нужна точка с запятой

// ПРАВИЛЬНО:
$func = function($x) {
    return $x * 2;
};

Это частая ошибка новичков. Присваивание переменной — это оператор, он должен заканчиваться точкой с запятой.

Ошибка 2: Захват переменной без use

$factor = 5;
$multiply = function($x) {
    // Ошибка! $factor не определена внутри функции
    return $x * $factor;
};

// Правильно:
$multiply = function($x) use ($factor) {
    return $x * $factor;
};

В PHP лямбда не видит внешние переменные автоматически. Нужно явно перечислить их в use.

Ошибка 3: Путаница с захватом по значению и по ссылке

$counter = 0;
$increment = function() use ($counter) {
    $counter++; // Изменяется копия, оригинал не меняется
};

$increment();
echo $counter; // 0, а не 1!

// Правильно:
$increment = function() use (&$counter) {
    $counter++;
};

По умолчанию переменные захватываются по значению (копируются). Чтобы изменять оригинал, используйте &.

Ошибка 4: Использование $this в лямбде

class MyClass {
    private $data = "секрет";
    
    public function getProcessor() {
        // В PHP 5.4+ $this доступен автоматически
        return function($x) {
            return $this->data . $x;
        };
    }
}

В старых версиях PHP (до 5.4) нужно было писать use ($this). Сейчас $this доступен автоматически, но только если лямбда определена внутри метода класса.

Ошибка 5: Слишком сложная логика в стрелочной функции

// НЕПРАВИЛЬНО: стрелочная функция не поддерживает множественные операторы
$process = fn($x) => {
    $temp = $x * 2;
    return $temp + 1;
}; // Ошибка!

// ПРАВИЛЬНО: используйте обычную анонимную функцию
$process = function($x) {
    $temp = $x * 2;
    return $temp + 1;
};

Стрелочные функции (fn) могут содержать только одно выражение. Для нескольких операторов используйте function.

Ошибка 6: Рекурсия в лямбде

// Попытка сделать рекурсивную лямбду
$factorial = function($n) {
    if ($n <= 1) return 1;
    return $n * $factorial($n - 1); // Ошибка! $factorial ещё не определена
};

// Правильно: передаём ссылку на себя
$factorial = function($n) use (&$factorial) {
    if ($n <= 1) return 1;
    return $n * $factorial($n - 1);
};

echo $factorial(5); // 120

Для рекурсии нужно захватывать переменную по ссылке, иначе внутри лямбды она будет не определена.

Ошибка 7: Изменение массива во время итерации

$numbers = [1, 2, 3, 4, 5];

// Опасный код: удаляем элементы во время фильтрации
array_filter($numbers, function($n) use (&$numbers) {
    if ($n % 2 == 0) {
        unset($numbers[array_search($n, $numbers)]);
    }
    return true;
}); // Непредсказуемое поведение!

// Правильно: используйте array_filter для фильтрации
$even = array_filter($numbers, fn($n) => $n % 2 == 0);

Не изменяйте массив, который обрабатывается, внутри callback-функции. Это приводит к багам.

Лучшие практики и советы профессионалов

1. Используйте стрелочные функции для простых преобразований

Если ваша лямбда состоит из одного выражения, используйте fn. Это короче и читается легче.

// Плохо:
$result = array_map(function($x) { return $x * 2; }, $array);

// Хорошо:
$result = array_map(fn($x) => $x * 2, $array);

2. Давайте переменным с лямбдами осмысленные имена

// Плохо:
$f = function($x) { return $x > 0; };

// Хорошо:
$isPositive = function($x) { return $x > 0; };

Имя переменной должно объяснять, что делает функция.

3. Не злоупотребляйте вложенными лямбдами

Лямбда внутри лямбды внутри лямбды — это трудно читать. Если логика становится сложной, вынесите часть в отдельную именованную функцию.

// Плохо: три уровня вложенности
$result = array_map(
    fn($x) => array_filter(
        $x,
        fn($y) => $y > array_reduce($x, fn($a, $b) => $a + $b, 0) / count($x)
    ),
    $data
);

// Хорошо: разбиваем на шаги
$average = fn($arr) => array_sum($arr) / count($arr);
$aboveAverage = fn($arr) => array_filter($arr, fn($v) => $v > $average($arr));
$result = array_map($aboveAverage, $data);

4. Используйте типизацию для самодокументирования

// С типами понятнее, что принимает и возвращает функция
$process = function(int $x, string $label): string {
    return "$label: " . ($x * 2);
};

Типы помогают IDE и другим разработчикам понять ваш код.

5. Избегайте побочных эффектов в лямбдах

Лямбда должна принимать данные и возвращать результат, не изменяя внешние переменные (если это не является явной целью).

// Плохо: лямбда изменяет внешнюю переменную
$count = 0;
$data = array_map(function($x) use (&$count) {
    $count++;
    return $x * 2;
}, $array);

// Хорошо: используйте отдельную функцию для подсчёта
$count = count($array);
$data = array_map(fn($x) => $x * 2, $array);

6. Для сложной логики используйте именованные функции

Если лямбда занимает больше 5-10 строк, возможно, лучше создать обычную функцию с понятным именем.

// Вместо этого:
usort($users, function($a, $b) {
    // 15 строк сложной логики сравнения
    // ...
});

// Лучше:
function compareUsers($a, $b) {
    // 15 строк сложной логики сравнения
    // ...
}
usort($users, 'compareUsers');

7. Тестируйте лямбды отдельно

Если лямбда используется в нескольких местах, вынесите её в переменную и протестируйте отдельно.

$validateEmail = function($email) {
    return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
};

// Теперь можно использовать и тестировать
assert($validateEmail("test@example.com") === true);
assert($validateEmail("invalid") === false);

$validEmails = array_filter($emails, $validateEmail);

Вопросы для самопроверки

  1. Что такое лямбда-функция? Чем она отличается от обычной функции?

    Ответ: Лямбда-функция — это функция без имени. Она создаётся «на лету» и может быть присвоена переменной, передана как аргумент или возвращена из другой функции. В отличие от обычной функции, она не регистрируется в глобальной таблице функций и существует только в момент выполнения.

  2. Какой синтаксис используется для создания лямбды в PHP?

    Ответ: Два основных способа: $func = function($args) { тело }; (анонимная функция) и $func = fn($args) => выражение; (стрелочная функция, PHP 7.4+).

  3. Для чего нужно ключевое слово use в лямбда-функции?

    Ответ: use позволяет захватить переменные из внешней области видимости и сделать их доступными внутри лямбды. Без use лямбда не видит внешние переменные.

  4. В чём разница между захватом переменной по значению и по ссылке?

    Ответ: По умолчанию (use ($var)) переменная копируется. Изменения внутри лямбды не влияют на оригинал. С ссылкой (use (&$var)) лямбда работает с оригинальной переменной, и изменения видны снаружи.

  5. Какие встроенные функции PHP принимают лямбды в качестве аргумента?

    Ответ: Основные: array_map, array_filter, array_reduce, usort, uasort, uksort, array_walk. Также многие функции для работы с коллекциями.

  6. Можно ли использовать лямбду для рекурсии?

    Ответ: Да, но нужно захватить переменную по ссылке: $factorial = function($n) use (&$factorial) { ... };. Иначе внутри лямбды переменная будет не определена.

  7. В чём ограничение стрелочных функций (fn)?

    Ответ: Стрелочная функция может содержать только одно выражение. Нельзя использовать несколько операторов, циклы, условные конструкции (if/else) — только тернарный оператор. Также нельзя захватывать переменные по ссылке.

  8. Что такое замыкание (closure)?

    Ответ: Замыкание — это лямбда-функция, которая «запоминает» переменные из внешней области видимости, в которой она была созда