[Hexlet] PHP: Функции

Ранее, я показывал пример того, как в PHP работают с современными фреймворками:

<?php

use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;

require 'vendor/autoload.php';

$app = new \Slim\App;
$app->get('/hello/{name}', function (Request $request, Response $response, array $args) {
    $name = $args['name'];
    $response->getBody()->write("Hello, $name");

    return $response;
});
$app->run();

В этом коде еще много новых синтаксических конструкций, но я хочу заострить внимание только на одной. Обратите внимание на странное определение функции. Во-первых, у нее нет имени, а во-вторых, функция определяется в том месте, куда должен передаваться второй параметр функции get . Это так называемая анонимная или лямбда функция. Лямбда функции в PHP являются объектами первого рода. В свою очередь, функция get представляет из себя функцию высшего порядка.

Если вы ничего не поняли из предыдущих предложений - не отчаивайтесь. Данный курс создан для того, чтобы разобраться с функциями по-настоящему. Все, что было до этого курса, всего лишь верхушка айсберга. Теперь мы копнем по-взрослому.

Кроме того, мы рассмотрим большое число часто используемых функций, которых нет в PHP, но есть в сторонних библиотеках. Некоторые из этих библиотек научимся использовать и будем это делать в дальнейших курсах. Мы изучим новый подход к обработке коллекций и выйдем на другой уровень понимания.

Основные понятия данного курса:

  • Детерминированность
  • Побочные эффекты
  • Splat operator
  • Объекты первого рода
  • Функции высшего порядка (map/filter/reduce)
  • Функциональное программирование

Не все из перечисленных тем и возможностей используются в повседневной жизни PHP разработчика (в других языках может быть совсем по-другому), но знать про них нужно. Во-первых, подходы, разбираемые в этом курсе, не являются специфичными для PHP. Зная их, вы гораздо легче сможете переключаться на другие языки. Особенно это касается JS, в котором используется сильно отличающийся от PHP стиль программирования. Во-вторых, работа с функциями, сильно прокачивает общий уровень разработчика.

PHP: Функции Чистые функции

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

Детерминированность

Стандартная функция rand , вызванная без аргументов, возвращает некоторое случайное число.

<?php

rand(); // => 151273074
rand(); // => 1129177627

Функция нужная и полезная, но неудобная в отладке и тестировании. Связано это с тем, что для одних и тех же входных аргументов (отсутствие аргументов так же попадает под это понятие), она может возвращать разные значения. Функции с таким поведением называются недетерминированными. К ним, в том числе, относятся функции, оперирующие системным временем.

<?php

// возвращает текущий год
date('Y'); // => 2018

Хотя прямо сейчас повторный запуск вернет точно такое же значение, через год оно уже будет другим (2019). То есть недетерминированной функция считается в том случае, когда она ведет себя так хотя бы единожды.

Детерминированные функции, напротив, ведут себя предсказуемо. Для одних и тех же входных данных, они всегда выдают один и тот же результат. Именно такими являются функции в математике. Для одного и того же x результат работы функции y = f(x) будет один и тот же. Интересно то, что, например, функция print_r детерминированная. Дело в том, что она всегда возвращает одно и тоже значение для любых входных данных. Это значение true , а не то, что печатается на экран, как можно было бы подумать. Печать на экран - побочный эффект, о нем мы поговорим чуть позже.

<?php

var_dump(print_r('lala'));
bool(true)

Понятие “Детерминированность” не ограничивается программированием или математикой. Сквозь него можно рассматривать практически любой процесс. Например, подбрасывание монетки — недетерминированный процесс, его результат всегда случаен.

Почему это понятие так важно? Детерминированность дает нам предсказуемость, а это, прямым образом, влияет на количество состояний, которые надо обрабатывать и предусматривать. В конечном итоге, код становится проще, а логика прямолинейнее. Пример: создание чего-либо на сайтах в интернете, обычно, недетерминированный процесс. То есть, если быстро нажать два раза кнопку отправки формы, то во многих ситуациях можно получить два раза оставленный комментарий, два раза отправленные деньги. Разработчикам сайтов приходится защищаться, ставить защиту от двойных нажатий или производить проверку на созданность уже внутри приложения.

Другой пример - создание директории. В командной строке эта операция выполняется с помощью программы mkdir .

$ mkdir test
$ mkdir test
mkdir: test: File exists

Как видно из вывода выше, повторный запуск создания директории выдает ошибку, так как она уже создана. Получается, нам нужно учитывать два состояния:

  1. Директории не существовало
  2. Директория существует

В случае детерминированной версии mkdir об этом можно было бы не думать.

Понятие детерминированности играет огромную роль в администрировании, в задачах связанных с программной настройкой серверов (configuration managmenet), выкладкой ПО и обновлениями. Ключевые слова: docker, immutable infrastructure, ansible.

Функция становится недетерминированной и в том случае, если она обращается не только к своим аргументам, но и некоторым внешним данным, например глобальным переменным, переменным окружения и так далее. Так происходит потому, что внешние данные могут измениться, и функция начнет выдавать другой результат даже если в нее передаются одни и те же аргументы.

<?php

function getCurrentShell()
{
    // Функция getenv обращается к указанной переменной окружения
    return getenv('SHELL'); // => /bin/bash
}

В общем случае нельзя сказать, что отсутствие детерминированности — абсолютное зло. Более того, многих полезных программ попросту бы не существовало без возможности писать недетерминированный код. Общая рекомендация при работе с детерминированностью звучит следующим образом. Если есть возможность написать функцию так, что она будет детерминированной, то так и делайте. Не используйте глобальных переменных, создавайте, функции зависящие только от своих собственных аргументов.

Побочные эффекты (side effects)

Вторая ключевая характеристика функций - наличие побочных эффектов. Побочным эффектом называют любые действия изменяющие среду выполнения. К ним относятся любые файловые операции, такие как запись в файл, отправка или прием данных по сети, даже вывод в консоль или чтение файла. Кроме того, побочными эффектами считаются обращения к глобальным переменным (как на чтение, так и запись) и изменение входных аргументов в случае, когда они передаются по ссылке. Вызов функции с побочными эффектами также считается побочным эффектом.

<?php

function sayHiTo($name)
{
    print_r("Hi, {$name}");
}

С другой стороны, любые вычислительные операции не являются побочными эффектами. Например, функция, суммирующая два переданных аргументами числа.

<?php

function sum($num1, $num2)
{
    return $num1 + $num2;
}

Без побочных эффектов невозможно написать ни одной полезной программы. Какие бы важные вычисления она ни делала, их результат должен быть как-то продемонстрирован. В самом простом случае его нужно вывести на экран, что автоматически приводит нас к побочным эффектам.

<?php

print_r(2 ** 5);

Побочные эффекты составляют одну из самых больших сложностей при разработке. Их наличие значительно затрудняет тестирование и отладку. Приводит к возникновению огромного числа ошибок (только при работе с файлами количество возможных ошибок измеряется сотней, начиная с того, что закончилось место на диске, заканчивая попыткой читать данные из несуществующего файла). Для их предотвращения код обрастает большим числом проверок и защитных механизмов.

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

Например, программа, которая конвертирует файл из текстового формата в pdf, в идеале выполняет ровно два побочных эффекта:

  1. Читает файл в самом начале работе программы.
  2. Записывает результат работы программы в новый файл.

Между этими двумя пунктами и происходит основная работа, которая содержит чистую алгоритмическую часть. Следовательно, побочные эффекты будут находиться только в верхнем слое приложения, а ядро, выполняющее основную работу, останется чистым от них.

Инкремент и декремент — единственные базовые арифметические операции в PHP, которые обладают побочными эффектами (изменяют само значение в переменной). Именно поэтому с ними сложно работать в составных выражениях. Они могут приводить к таким сложноотловимым ошибкам, что во многих языках вообще отказались от их введения (в Ruby и Python их нет), а в JS стандарты кодирования предписывают их не использовать.

Чистые функции

Идеальная функция с точки зрения удобства работы с ней называется чистой (pure). Чистая функция — это детерминированная функция, которая не производит побочных эффектов. Такая функция зависит только от своих входных аргументов и всегда ведет себя предсказуемо. Такие функции на 100% соответствуют своим математическим аналогам и могут рассматриваться как математические функции.

Чистые функции обладают рядом ключевых достоинств:

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

В хорошо спроектированных программах, побочные эффекты стараются изолировать в небольшой части приложения так, чтобы большая часть кода была чистой.

Прямо сейчас сказанное выше может звучать довольно абстрактно, что понятно. Осознание этой темы требует не только общего понимания прочитанного, но и набитых шишек, связанных со сложностью работы в мешанине побочных эффектов. Тему чистоты мы будем поднимать регулярно. Особенно сильно она прорабатывается в проектах, на живом коде.

Дополнительные материалы

  1. Побочные эффекты
  2. Детерминированная функция

PHP: Функции Разделение команд и запросов

Command-query Separation (CQS) — принцип программирования, изобретенный Бертандом Майером, создателем языка Eiffel.

Он утвеждает, что каждая функция является либо командой которая выполняет действие (action), либо запросом (query) который извлекает данные, но не тем и другим одновременно. Команда всегда связана с выполнением побочных эффектов, а чистые функции возможны только для запросов.

Команда

<?php

// Возвращает true или false как результат своего выполнения
save($user);

Согласно принципу CQS, функция save является командой. Единственное что она может возвращать (опять же согласно принципу) - успешность своего выполнения, то есть true или false , либо null , как, например, в случае с print_r . Возврат этой функцией любых осмысленных данных, рассматривается как нарушение CQS. Однако, стоит сказать, что существуют ситуации в которых невозможно соблюсти этот принцип. Например открытие файла на запись возвращает файловый дескриптор (идентификатор через который происходят манипуляции с файлом).

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

<?php

$file = fopen('/etc/hosts', 'r');

Запрос

<?php

// Возвращает true или false
isAdmin($user);

Функция isAdmin - предикат, типичный запрос (query) или, можно даже сказать, вопрос, который звучит так “Пользователь администратор?” Такая функция, с точки зрения CQS, не может изменять состояние системы, например, поменять дату проверки на администоратора внутри пользователя или даже сделать пользователя администратором. Это противоречит не только CQS, но и здравому смыслу. В отличие от предыдущего примера, true и false в случае предикатов, это не успешность выполнения функции, а ответ на заданный вопрос. CQS имеет альтернативную формулировку, которая отлично характеризует код выше: “Задавая вопрос, не изменяй ответ”. К запросам относятся и любые вычисления.

<?php

$max = max([1, 30, 4]);

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

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

Дополнительные материалы

  1. Command-query Separation
  2. Принцип наименьшего удивления

PHP: Функции Упаковка аргументов

Сигнатура функции array_merge в документации определяется так:

array array_merge ( array $array1 [, array $... ] )

Она говорит нам о том, что в array_merge можно передать любое количество массивов:

<?php

array_merge([1]);
# => Array
# (
#     [0] => 1
# )
array_merge([1], [1]);
# => Array
# (
#     [0] => 1
#     [1] => 1
# )
array_merge([1], [1], [3, 4]);
# => Array
# (
#     [0] => 1
#     [1] => 1
#     [2] => 3
#     [3] => 4
# )
array_merge([1], [1], [3, 4], []);
# => Array
# (
#     [0] => 1
#     [1] => 1
#     [2] => 3
#     [3] => 4
# )

С точки зрения вызова - ничего необычного, просто разное число аргументов. А вот определение функции с переменным числом аргументов выглядит необычно и использует незнакомый для нас синтаксис:

<?php

function sum(...$numbers)
{
    return array_sum($numbers);
}

echo sum(9, 4); // => 13
echo sum(-3, 0, 3, 1); // => 1

Три точки представляют собой Splat Operator . В PHP на текущий момент он может использоваться только в определениях и вызовах функций (в других языках его можно использовать значительно шире, в том числе при дестракчиринге). Основная сложность для понимания состоит в том, что Splat Operator выполняет различные действия в зависимости от того, где он применяется. В определении функции он выполняет “упаковку” параметров, а при вызове - наоборот, “распаковку”. Про упаковку параметров будем говорить в этом уроке. В следующем же уроке рассмотрим Splat Operator в роли распаковщика.

Итак, назначение Splat Operator в определении функции состоит в том, чтобы собрать в массив все переданные аргументы. Если в функцию не передается ни одного аргумента, то массив будет пустым.

<?php

echo sum(); // => 0

Обратите внимание на то, что этому оператору не важен тип аргументов, они все станут элементами массива, даже если мы передаем на вход функции массивы.

<?php

function show(...$arguments)
{
    print_r($arguments);
}

show([]);
# Array
# (
#     [0] => Array
#         (
#         )
#
# )

show([1, 3], [], 3);
# => Array
# (
#     [0] => Array
#         (
#             [0] => 1
#             [1] => 3
#         )
#
#     [1] => Array
#         (
#         )
#
#     [2] => 3
# )

Теперь взглянем на сигнатуру array_merge еще раз:

array array_merge ( array $array1 [, array $... ] )

Видно, что функция array_merge ждет на вход как минимум один массив, опциональны только следующие. Такого поведения можно добиться следующим кодом:

<?php

function sum($a, ...$numbers)
{
    return $a + array_sum($numbers);
}

echo sum();
// => PHP Fatal error:  Uncaught ArgumentCountError: Too few arguments to function sum(), 0 passed

echo sum(10); // => 10
echo sum(10, 4); // => 14
echo sum(8, 10, 4); // => 22

Тоже можно сделать и для двух аргументов:

<?php

function sum($a, $b, ...$numbers)
{
    # ...
}

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

<?php

function sum(...$numbers, $a)
{
    # ...
}

И такой тоже:

<?php

function sum($a, ...$numbers, $a)
{
    # ...
}

PHP: Функции Распаковка аргументов

Splat Operator в вызовах функций синтаксически идентичен Splat Operator в определениях, но выполняет обратное действие:

<?php

$arrayOfArrays = [
    [1, 2],
    [2, 3]
];

array_merge(...$arrayOfArrays);
# => Array
# (
#     [0] => 1
#     [1] => 2
#     [2] => 2
#     [3] => 3
# )

Другими словами, Splat Operator раскладывает массив на аргументы. Количество аргументов, полученных Splat Operator, равно количеству элементов массива. По сути код выше преобразуется в вызов:

<?php

array_merge($arrayOfArrays[0], $arrayOfArrays[1]);
// array_merge([1, 2], [2, 3]);

Как и в случае с определением функций, Splat Operator может использоваться совместно с позиционными аргументами:

<?php

array_merge([3], ...$arrayOfArrays);
# => Array
# (
#     [0] => 3
#     [1] => 1
#     [2] => 2
#     [3] => 2
#     [4] => 3
# )

Тоже самое справедливо и для большего количества аргументов:

<?php

$array = [3, 2];
array_merge([3], $array, ...$arrayOfArrays);
# => Array
# (
#     [0] => 3
#     [1] => 3
#     [2] => 2
#     [3] => 1
#     [4] => 2
#     [5] => 2
#     [6] => 3
# )

И точно так же должен всегда оказываться последним аргументом.

<?php

array_merge(...$arrayOfArrays, [3, 2]);
// => Fatal error: Cannot use positional argument after argument unpacking

В PHP Splat Operator применяется не каждый день, но иногда бывает нужен, если аргументы оказываются записаны в массив.

PHP: Функции Полезные функции для работы с коллекциями

Стандартная библиотека в PHP небогата функциями для работы с коллекциями, строками или датами. Этот недостаток можно восполнить, подключив стороннюю библиотеку. Например, в мире JS, есть стандарт де-факто, без которого не обходится практически ни один проект - lodash. В PHP нет единого устоявшегося решения, но есть пачка небольших, которые используются в разных проектах. Часть из них основана на объектном синтаксисе, и мы рассмотрим их в более поздних уроках, а часть представляет из себя набор обычных функций. Кроме того, часть функций относится к функциям высшего порядка, которые мы еще не проходили, но разберем уже в следующем уроке.

Объектные:

Использующие только функции:

Скажу сразу, что необходимость использования такой библиотеки в реальной работе возникает практически сразу. Те, кто не знает про них, постоянно пишут велосипеды. Еще не так давно программисты в PHP создавали файлики с полезными функциями и таскали их из одного проекта в другой. Хорошо, что эти времена уже прошли и можно не тратить время на то, что уже делалось тысячу раз до нас. Однако, мы не сможем разобрать все возможные функции в рамках наших курсов. Кое-что рассмотрим, но в дальнейшем не забывайте периодически просматривать их на наличие чего-либо полезного.

Обратите внимание на документацию указанных библиотек. Зачастую они повторяют те функции, которые уже встроены в сам язык. Делается это по разным причинам. Вот некоторые из них:

  1. Консистентность (согласованность). Функции делают для того, чтобы библиотека была полной.
  2. Исправление ошибок PHP. Некоторые функции в PHP по историческим причинам иногда ведут себя неверно.
  3. Улучшение. Другой порядок аргументов, расширенные возможности, убранные ограничения или просто понятное имя.

Итак, поехали. Библиотека Funct.

Collections

last

Такая простая и нужная функция почему-то отсутствует в самом PHP.

<?php

\Funct\Collection\last([1, 2, 3]); // => 3

rest

Принимает на вход массив и возвращает новый массив, в котором отсутствует первый элемент исходного.

<?php

\Funct\Collection\rest([5, 4, 3, 2, 1]); // => [4, 3, 2, 1]

without

Возвращает копию массива, из которого удалены все значения, переданные в функцию вторым и последующими параметрами.

<?php

\Funct\Collection\without([1, 2, 1, 0, 3, 1, 4], 0, 1); // => [2, 3, 4]

flattenAll

“Выпрямляет” вложенный массив, делая его плоским.

<?php

\Funct\Collection\flattenAll(['a', ['b', ['c', ['d']]]]); // => ['a', 'b', 'c', 'd']

union

Находит объединение множеств.

<?php

\Funct\Collection\union([1, 2, 3], [101, 2, 1, 10], [2, 1]); // => [1, 2, 3, 101, 10]

findWhere($collection, $value)

Просматривает массив и возвращает первое значение, совпадающее по всем парам «ключ-значение», переданным вторым параметром.

<?php

\Funct\Collection\findWhere(
    [
        ['title' => 'Book of Fooos', 'author' => 'FooBar', 'year' => 1111],
        ['title' => 'Cymbeline', 'author' => 'Shakespeare', 'year' => 1611],
        ['title' => 'The Tempest', 'author' => 'Shakespeare', 'year' => 1611],
        ['title' => 'Book of Foos Barrrs', 'author' => 'FooBar', 'year' => 2222],
        ['title' => 'Still foooing', 'author' => 'FooBar', 'year' => 3333],
        ['title' => 'Happy Foo', 'author' => 'FooBar', 'year' => 4444],
    ],
    ['author' => 'Shakespeare', 'year' => 1611]
); // => ['title' => 'Cymbeline', 'author' => 'Shakespeare', 'year' => 1611]

Strings

camelize

Принимает на вход строку и возвращает ее версию, записанную в camelCase нотации.

<?php
\Funct\Strings\camelize('data_rate'); //'dataRate'
\Funct\Strings\camelize('background-color'); //'backgroundColor'
\Funct\Strings\camelize('-moz-something'); //'MozSomething'
\Funct\Strings\camelize('_car_speed_'); //'CarSpeed'
\Funct\Strings\camelize('yes_we_can'); //'yesWeCan

contains

Проверяет, включает ли строчка подстроку.

<?php
\Funct\Strings\contains('PHP is one of the best languages!', 'one'); // true

endsWith

Проверяет, оканчивается ли строчка на подстроку.

<?php

\Funct\Strings\endsWith("hello jon", 'jon'); // => true

В этом уроке перечислена лишь небольшая часть из всего того многообразия, которое вы можете найти на гитхабе, в документации соответствующих библиотек. Изучить все функции в рамках курсов невозможно да и не нужно. Достаточно переодически проходиться по документации популярных библиотек и просматривать то как они используются. Так, постепенно, вы научитесь писать более понятный и лаконичный код.

Дополнительные материалы

  1. Функции стандартной библиотеки

PHP: Функции Объекты первого класса

В языках программирования существует понятие «объекты первого рода (или класса)». Им обозначают элементы, которые могут быть переданы в функции, возвращены из функций и присвоены переменным. К таким элементам относятся любые данные, например числа, строки, массивы или логические значения. Переменная под это понятие не попадает, объектом первого рода считаются те данные, которые лежат в переменной.

<?php

$num = 5; // 5 - объект первого рода

// 2 - объект первого рода, $result, $num - содержимое переменных объекты первого рода
$result = pow($num, 2)

Описанное выше вы проделывали множество раз, и эту тему можно было бы не поднимать, если бы не одно «но». Функции тоже могут быть объектами первого рода:

Запустите код выше. Он работает! Давайте разбираться.

Мы привыкли к такому определению функций:

<?php

function greeting()
{
    // ...
}

У функции есть имя, которое указывается после ключевого слова function , а сама конструкция является инструкцией (кстати, поэтому в конце нет точки запятой). Мы не можем написать так:

<?php

// переменная содержащая определение функции?
$fn = function greeting()
{
    // ...
}

Так как определение функции - инструкция, то оно не может подставляться в те места, где ожидаются выражения. Присваивание как раз подразумевает то, что в правой части находится именно выражение. Соответственно, такой код падает с ошибкой. А следующий код уже работает.

<?php

$func = function () {
    echo "For hands of gold are always cold. But a woman's hands are warm";
};

Даже не вникая в синтаксис можно делать вывод о том, что конструкция справа от «равно» - выражение. И это выражение порождает функцию. В PHP подобные функции называют анонимными, потому что у них нет имени. Глядя на код выше нужно понимать, что определение функции и ее присваивание переменной - две разных операции. Чистое определение выглядит так:

<?php

function () {
    echo "For hands of gold are always cold. But a woman's hands are warm";
};

Очень похоже на обычную функцию за исключением отсутствия имени. Правда, есть еще и стилевые отличия, закрепленные стандартом PSR-2. В других языках анонимные функции нередко называют лямбда-функциями. В принципе, и в PHP их иногда зовут также.

<?php

$func();

Имя переменной можно выбирать произвольно, в этом смысле анонимные функции никак не влияют на ситуацию. Более того, в отличие от именованных функций, которые будучи единожды созданными - никуда не изчезают в процессе жизни программы, анонимные функции постоянно создаются и пропадают вместе с переменными, в которые они записаны. Другими словами, анонимные функции всегда локальны относительно контекста и доступны там, где они были созданы если не предпринимать специальных мер по их возврату.

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

<?php

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

$sum(1, 4); // => 5

А раз анонимная функция — выражение, мы можем определять ее в любом месте программы, допускающем использование выражений, например в теле другой функции!

<?php

function sum($a, $b)
{
    // определяем анонимную функцию
    $sum = function ($a, $b) {
        return $a + $b;
    };
    // вызываем анонимную функцию и возвращаем результат ее выполнения
    return $sum($a, $b);
}

sum(1, 4); // => 5

Главное в коде выше не забыть поставить return и помнить, что $a и $b снаружи анонимной функции не связаны с переменными, имеющими те же имена внутри анонимной функции.

Думаю что сейчас в вашей голове возник вопрос «зачем все это? жили же как-то раньше и сейчас проживем». Анонимные функции появились в PHP не сразу, но все же появились. Их использование значительно повышает выразительные возможности языка, и в этом вы скоро убедитесь. Если же взять JS, то там анонимные функции составляют костяк любой программы. Функции, создающие функции, возвращающие функции и принимающие функции как аргументы - основной способ разрабатывать в JS.

Передача обычных функций

Несмотря на то, что обычные функции в PHP не являются объектами первого рода, PHP может имитировать соответствующее поведение с помощью строк. Достаточно записать имя функции в переменную и вызвать ее.

<?php

$fn = 'strlen';
print_r($fn('clojure for brave')); // => 17

Точно так же можно осуществить “передачу” функции в функцию.

<?php

function call(string $fn, $argument)
{
    return $fn($argument);
}

$result = call('strlen', 'haskell is power!');
print_r($result); // => 16

Дополнительные материалы

  1. Документация

PHP: Функции Функции высшего порядка

Пока мы рассмотрели только одну возможность объектов первого рода применительно к функциям — присвоение переменной. Самое интересное начинается, когда мы передаем одни функции в другие функции.

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

<?php

$users = [
    ['name' => 'Igor', 'age' => 19],
    ['name' => 'Danil', 'age' => 1],
    ['name' => 'Vovan', 'age' => 4],
    ['name' => 'Matvey', 'age' => 16],
];

При таких условиях функция sort становится абсолютно бесполезной, потому что она может сортировать только списки примитивных типов данных. Но выше я описал только лишь одну из тысяч возможных ситуаций. Мы можем захотеть сортировать по любому параметру (или даже по набору параметров) и в любом порядке. Сортировки нужны часто, и многие из них довольно сложны. Худшее, что можно начать делать — реализовывать функцию sort под каждую ситуацию. Так что же делать? Если покопаться в документации PHP, то можно обнаружить функцию usort. Ее определение звучит так:

Сортирует массив по значениям, используя пользовательскую функцию для сравнения элементов

bool usort ( array &$array , callable $value_compare_func )

Эта функция сортирует элементы массива, используя для сравнения значений callback-функцию, предоставленную пользователем. Используйте эту функцию, если вам нужно отсортировать массив по какому-нибудь необычному признаку. Слово callback означает то, что наша задача — передать функцию (но не вызывать!), а вызывать ее будет функция usort .

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

usort выполняет всю работу по непосредственному перемещению элементов в массиве, но то, какой элемент больше или меньше, — зависит от вас. Достигается подобная схема за счет той самой анонимной функции, передающейся вторым параметром. Эта функция принимает на вход два параметра — usort отдает в нее два элемента, которые она сравнивает в данный момент. В нашем случае элементы — пользователи. Ваша задача — внутри этой функции посчитать, что больше или меньше, и сделать следующее. Если элементы равны, то вы должны вернуть 0 , если первый элемент больше второго, то считается, что они отсортированы правильно, и вы должны вернуть 1 , иначе возвращается -1 , а usort производит их сортировку.

Из кода выше видно, что внутри функции сравнение идет по свойству age переданных пользователей. Нетрудно догадаться, что эта функция вызывается внутри usort множество раз (а именно на каждое сравнение). Как только она начнет возвращать 1 для каждой пары элементов — сортировка завершена.

Функция usort относится к так называемым функциям высшего порядка (high order functions). Функции высшего порядка — это функции, которые либо принимают, либо возвращают другие функции, либо делают все сразу. Такие функции, как правило, реализуют некий обобщенный алгоритм (например, сортировку), а ключевую часть логики делегируют вам через анонимную функцию. Главный плюс от применения таких функций — серьезное повышение коэффициента повторного использования кода.

В примере выше не обязательно создавать переменную для функции. Говоря откровенно, их вообще редко записывают в переменные. Типичное использование выглядит как прямая передача функции в функцию:

<?php

usort($users, function ($a, $b) {
    if ($a['age'] == $b['age']) {
        return 0;
    }
    return $a['age'] > $b['age'] ? 1 : -1;
});

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

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

<?php

function say(callable $fn) {
    echo $fn();
}

say(function () {
    return 'hi!';
}); // => hi!

Функция say делает вызов функции, находящейся внутри переменной $fn . В нашем примере функция возвращает строку, которая тут же выводится на экран.

Функции высшего порядка настолько удобны в большинстве языков, что практически целиком могут заменить использование тех же циклов. Например, канонический код на JS выглядит так:

const getJsFiles = dir => fs.readdirSync(dir)
  .filter(file => file.endsWith('js'))
  .map(file => path.resolve(dir, file));

В этом коде присутствует 2 функции высшего порядка ( filter и map ), 3 анонимные функции и два прохода (это делают функции высшего порядка) по содержимому директории dir . Подобный код на PHP, с циклами и без, займет примерно в 4 раза больше строк даже при использовании специальных библиотек, упрощающих использование функций высшего порядка (это те же библиотеки, которые мы рассматривали в курсе). Связано это с многословностью синтаксиса PHP.

В следующих уроках мы рассмотрим три самые главные функции высшего порядка, которыми можно решать практически любые задачи. Две из них используются в примере выше, это map и filter , а третья — reduce (ее еще называют fold ). Они все доступны в стандартной библиотеке PHP.

Дополнительные материалы

  1. PHP The Right Way

PHP: Функции Отображение (map)

Первая функция из золотой тройки называется map . Ее название переводится на русский как “отображение”, что точно отражает суть выполняемой операции. Если в программировании говорят об отображении, то всегда подразумевают функцию map . Ее можно найти практически в любом языке, и везде это будет одна и та же операция. В PHP она имеет немного отличающееся название - array_map.

Попробуем прийти к этой функции через примеры. Возьмем список пользователей из предыдущего урока и извлечем из него имена всех пользователей.

<?php
$users = [
    ['name' => 'Igor', 'age' => 19],
    ['name' => 'Danil', 'age' => 1],
    ['name' => 'Vovan', 'age' => 4],
    ['name' => 'Matvey', 'age' => 16],
];

$result = [];
foreach ($users as ['name' => $name]) {
    $result[] = $name;
}
print_r($result); // => ['Igor', 'Danil', 'Vovan', 'Matvey']

Здесь мы видим обычную агрегацию с использованием foreach . А что, если нам понадобится извлечь возраст? Повторяем:

<?php

$result = [];
foreach ($users as ['age' => $age]) { // destructuring
    $result[] = $age;
}
print_r($result); // => [19, 1, 4, 16]

В примерах выше легко увидеть закономерность. Выполняется один и тот же проход по циклу, и результат собирается в переменную $result . Единственное, что меняется — значение, которое мы извлекаем из элементов исходного массива. Именно этот паттерн называется отображением.

Теперь посмотрим как выполнить ту же самую операцию, используя array_map :

<?php

$names = array_map(function ($user) {
    return $user['name'];
}, $users);

print_r($names); // => ['Igor', 'Danil', 'Vovan', 'Matvey']

array_map принимает первым параметром анонимную функцию и вторым — коллекцию, которая будет отображаться. Дальше внутри себя array_map делает перебор элементов переданной коллекции и для каждого элемента вызывает ту самую анонимную функцию. На вход этой функции передается элемент исходного массива, а ее результат записывается в новый массив, который и возвращается наружу. Некоторые замечания относительно функции array_map (они относятся в принципе к операции отображения):

  • Возвращаемый массив всегда такого же, размера как и исходный. array_map “отображает” каждый элемент исходного массива в элемент возвращаемого.
  • Внутри анонимной функции можно выполнять любые вычисления, а не только возвращать часть исходного элемента
  • array_map полностью скрывает от нас наличие обхода и процесс сборки нового массива.

Типичный пример, который любят приводить в документации функции к map разных языков программирования, применение некоторой арифметической операции к каждому элементу коллекции.

<?php

$numbers = [5, 2, 3];

$newNumbers = array_map(function ($number) {
    // возводим в квадрат каждое число
    return $number ** 2;
}, $numbers);

print_r($newNumbers); // => [25, 4, 9]

Пример выглядит искуственно, но хорошо отражает суть операции.

Реализация

<?php

function myMap(callable $callback, $coll)
{
    $result = [];
    foreach ($coll as $item) {
        $result[] = $callback($item);
    }
    return $result;
}

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

PHP: Функции Фильтрация (filter)

Следующая операция называется “фильтрация” и выполняется она в PHP с помощью функции array_filter (в других языках ее называют просто filter или select ). Понятие “фильтрация”, интуитивно понятно каждому человеку. Мы пьем фильтрованную воду и фильтруем то что говорим. В программировании практически тоже самое. Операция “фильтрация”, по отношению к коллекции, означает что мы удаляем из нее нежелательные элементы. Возьмем наших многострадальных пользователей. Типичная задача может выглядеть так, выберем пользователей старше 10 лет.

<?php
$users = [
    ['name' => 'Igor', 'age' => 19],
    ['name' => 'Danil', 'age' => 1],
    ['name' => 'Vovan', 'age' => 4],
    ['name' => 'Matvey', 'age' => 16],
];

$result = [];
foreach ($users as $user) {
    if ($user['age'] > 10) {
        $result[] = $user;
    }
}
print_r($result);
// => Array
// (
//     [0] => Array
//         (
//             [name] => Igor
//             [age] => 19
//         )
//
//     [1] => Array
//         (
//             [name] => Matvey
//             [age] => 16
//         )
//
// )

Фильтрация встречается так же часто как отображение, а возможно и чаще. Общая схема кода при фильтрации практически один в один как и в отображении, кроме пары ключевых моментов:

  • Фильтрация возвращает коллекцию либо того же размера (если ничего не было отфильтровано) либо меньшего. Она может вернуть даже пустую коллекцию, если ни один из элементов не подошел.
  • Фильтрация всегда возвращает исходные элементы. Она никогда не делает отображение. Если на вход фильтрации поступил список пользователей, то список пользователей будет и на выходе.

Теперь посмотрим как выглядит фильтрация при использовании функции высшего порядка array_filter .

<?php

// Порядок аргументов обратный. Сначала коллекция, затем функция.
$users = array_filter($users, function ($user) {
    return $user['age'] > 10;
});

В отличие от array_map , функция, которая передается в array_filter должна быть предикатом. То есть ее задача вернуть либо true либо false для каждого элемента коллекции. Ее возврат никак не используется, он всего лишь говорит о том, включать ли текущий элемент в итоговый массив либо нет. Новички часто делают ошибку на этом этапе и начинают возвращать из фильтра то, что они бы хотели увидеть в результирующем массиве (для этого надо применять array_map ).

Реализация

<?php

function myFilter($coll, callable $callback)
{
    $result = [];
    foreach ($coll as $key => $item) {
        if ($callback($item)) { // Предикат используется только для проверки
            $result[$key] = $item; // В результат всегда добавляется элемент исходной коллекции
        }
    }
    return $result;
}

Обратите внимание на то, что array_filter сохраняет ключи. При работе с индексированными массивами такое поведение нежелательно, поэтому придется использовать функцию array_values для сброса порядка.

PHP: Функции Агрегация (reduce)

Последняя функция из нашей тройки - array_reduce используется для агрегации (название в других языках accumulate , fold или, по-русски, “свертка”). Она устроена немного сложнее, чем map и filter , но, в целом, сохраняет общий подход с передачей функции.

Как обычно, начнем с примера на циклах. Реализуем код, находящий самого взрослого пользователя.

<?php
$users = [
    ['name' => 'Igor', 'age' => 19],
    ['name' => 'Danil', 'age' => 4],
    ['name' => 'Vovan', 'age' => 4],
    ['name' => 'Matvey', 'age' => 16],
];

$oldest = $users[0];
foreach ($users as $user) {
    if ($user['age'] > $oldest['age']) {
        $oldest = $user;
    }
}
print_r($oldest); // => ['name' => 'Igor', 'age' => 19]

Основное отличие агрегации от отображения и фильтрации в том, что результатом агрегации может быть любой тип данных, как примитивный так и составной, например, массив. Кроме того, агрегация нередко подразумевает инициализацию начальным значением. В примере выше она выполняется на строчке $oldest = $users[0]; .

Посмотрим еще один пример агрегации: группировка имен пользователей по возрасту.

<?php
$users = [
    ['name' => 'Igor', 'age' => 19],
    ['name' => 'Danil', 'age' => 4],
    ['name' => 'Vovan', 'age' => 4],
    ['name' => 'Matvey', 'age' => 16],
];

$usersByAge = [];
foreach ($users as $user) {
    if (!array_key_exists($user['age'], $usersByAge)) {
        $usersByAge[$user['age']] = [];
    }
    $usersByAge[$user['age']][] = $user['name'];
}
print_r($usersByAge);
# => Array
# (
#     [19] => Array
#         (
#             [0] => Igor
#         )
#
#     [4] => Array
#         (
#             [0] => Danil
#             [1] => Vovan
#         )
#
#     [16] => Array
#         (
#             [0] => Matvey
#         )
#
# )

В этом примере результатом агрегации становится массив массивов, который в самом начале инициируется пустым массивом. Значение, которое накапливает результат агрегации, принятно называть словом “аккумулятор”. В примерах выше это $oldest и $usersByAge .

Реализуем первый пример используя array_reduce .

<?php

$oldest = array_reduce($users, function ($acc, $user) {
    return $user['age'] > $acc['age'] ? $user : $acc;
}, $users[0]);
print_r($oldest); // => ['name' => 'Igor', 'age' => 19]

Функция array_reduce принимает на вход три параметра. Два из них уже традиционны - это коллекция и функция-обработчик, а вот третьим выступает начальное значение аккумулятора. Поиск самого взрослого пользователя аналогичен поиску максимального (или минимального) числа в массиве. Соответственно, аккумулятор должен быть инициализирован первым пользователем. Этот же аккумулятор возвращается наружу.

Анонимная функция, передаваемая в array_reduce - самая важная часть и ключ к пониманию работы всего механизма агрегации. Она принимает на вход два значения. Первый - текущее значение аккумулятора, второй - текущий обрабатываемый элемент. Задача функции - вернуть новое значение аккумулятора. array_reduce никак не анализирует содержимое аккумулятора. Всё, что она делает, передает его в каждый новый вызов до тех пор, пока не будет обработана вся коллекция, и в конце концов вернет его наружу. Подчеркну, что возвращать аккумулятор надо всегда, даже если он не изменился.

Второй пример с использованием array_reduce выглядит так:

<?php

$usersByAge = array_reduce($users, function ($acc, $user) {
    if (!array_key_exists($user['age'], $acc)) {
        $acc[$user['age']] = [];
    }
    $acc[$user['age']][] = $user['name'];

    return $acc;
}, []);
print_r($usersByAge);

Код практически не изменился, за исключением того, что ушел цикл и появился возврат аккумулятора из анонимной функции.

Реализация

<?php

function myReduce($coll, callable $callback, $init = null)
{
    $acc = $init;
    foreach ($coll as $item) {
        $acc = $callback($acc, $item); // Заменяем старый аккумулятор новым
    }
    return $acc;
}

array_reduce - очень мощная функция. Формально, можно работать, используя одну лишь ее, так как она может заменить и отображение и фильтрацию . Но делать так не стоит. Агрегация управляет состоянием (аккумулятором) явно. Такой код всегда сложнее и требует больше действий. Поэтому, если задачу возможно решить отображением или фильтрацией, то так и нужно делать.

PHP: Функции Сигналы

Пример с usort хорошо демонстрирует важность и удобство функций высшего порядка для решения повседневных задач. Описав алгоритм один раз, мы можем получать различные варианты поведения специфицируя их функциями. Тоже самое относится к рассмотренным функциям map , filter и reduce . Но есть еще один важный аспект, который требует рассмотрения.

Типичный код на PHP выглядит так. Делается один цикл внутри которого выполняются сразу все операции и в конце получается некоторый интересующий нас результат. Ничего криминального, но такой код может быть сложен для анализа, потому что требует держать в голове сразу все операции. С другой стороны, при использовании функций высшего порядка принято разделять задачу на подзадачи и выполнять их последовательно друг за другом выстраивая в цепочку операций.

Предположим что мы пишем функцию, которая принимает на вход список путей файловой системы, находит среди них файлы с расширением php без учета регистра и возвращает имена этих файлов. Для решения этой задачи нам понадобятся следующие функции:

  • is_file - проверяет что переданный путь это реально существующий файл
  • pathinfo - позволяет извлекать расширение из имени файла
  • basename - извлекает имя файла из полного пути
<?php

function getPHPFileNames(array $paths)
{
    $result = [];
    foreach ($paths as $path) {
        $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
        if (is_file($path) && $extension === 'php') {
            $result[] = basename($path);
        }
    }

    return $result;
}

$names = getPHPFileNames(['index.php', 'wop.PHP', 'nonexists', 'node_modules']);
print_r($names);
# => Array
# (
#     [0] => index.php
#     [1] => wop.PHP
# )

В примере выше типовое решение с использованием цикла. Его алгоритм можно описать так:

  1. Просматриваем каждый путь
  2. Если текущий путь файл и его расширение php без учета регистра то добавляем в результирующий массив

Если попытаться тоже самое реализовать используя функцию array_reduce , то на выходе получится код идентичный решению на циклах. Но если хорошо подумать, то можно увидеть что эта задача распадается на две: фильтрацию и отображение.

<?php

function getPHPFileNames(array $paths)
{
    // фильтруем оставляя только подходящие пути
    $phpFiles = array_filter($paths, function ($path) {
        $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
        return is_file($path) && $extension === 'php';
    });

    // извлекаем из оставшихся путей имена файлов и возвращаем их наружу
    return array_map(function ($path) {
        return basename($path);
    }, $phpFiles);
}

$names = getPHPFileNames(['index.php', 'wop.PHP', 'nonexists', 'node_modules']);
print_r($names);

Код получился чуть короче (без учета комментариев), но и многословнее (это исключительно особенность php), но главное не его размер. С увеличением количества операций и их сложности, код разбитый таким образом читается и анализируется значительно проще, так как каждая операция выполняется независимо для всего набора сразу. В голове приходится держать меньше деталей и можно сразу увидеть то, как операция влияет на все данные. Однако, научиться правильно разбивать задачу на подзадачи не так просто, как может показаться в начале. Нужна некоторая практика и сноровка перед тем как ваш код станет удобоварим.

Сама возможность такого разбиения основывается на простой идее, которую иногда называют “стандартные интерфейсы”. Заключается она в том, что на входе и выходе из функций ожидается один и тот же тип данных, в нашем случае, массив. Это позволяет соединять функции и строить цепочки выполняющие большое количество разных задач, без необходимости реализовывать новые функции. Рассмотренные ранее операции — отображение, фильтрация и агрегация — комбинируясь друг с другом, позволяют решать подавляющее число задач по обработке коллекций. С чем-то подобным мы все встречались в своей жизни, когда собирали конструкторы Lego. Небольшое число примитивных деталей за счет одинаковых соединений позволяет строить конструкции практически неограниченной сложности.

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

Для сравнения я покажу задачу выше, реализованную на JS.

const getPHPFileNames = paths => paths
  .filter(filepath => fs.lstatSync(fullPath).isFile())
  .filter(filepath => path.extname(filepath).toLowerCase() === '.php')
  .map(filepath => path.basename(filepath));

const names = getPHPFileNames(['index.php', 'wop.PHP', 'nonexists', 'node_modules']);
console.log(names); // [index, wop]

Понятно, что он слишком непривычный, но ухватить фильтры и мапы внутри него не так сложно, да и в целом понять смысл того что происходит. Заметьте, что здесь я разбил фильтр на два. Учитывая лаконичность определения функции в js, гораздо лучше разбивать проверки на большее число фильтров чем делать один сложный фильтр.

Производительность

За кадром остался вопрос производительности. Возможно, кто-то из вас догадался, что на каждый вызов функции, обрабатывающей коллекцию, мы получаем проход по всему списку. Чем больше таких функций, тем больше проходов. Казалось бы код замедляется, зачем так делать? На практике дополнительные проходы практически никогда не проблема. Задачи, в которых требуется одномоментная обработка десятков и сотен тысяч элементов, встречаются крайне редко. Большая часть операций происходит со списками до тысяч элементов. А для такого списка одним проходом больше одним меньше - разницы, можно сказать, никакой.

Но это не вся правда. На самом деле, существуют специальные коллекции, которые в момент вызова функций фильтрации, отображения и т.п. не выполняют операции сразу. Они накапливают необходимые действия, а во время первого использования выполняют сразу все одним проходом. Это так называемые «ленивые коллекции».

Дополнительные материалы

  1. Обработка сигналов

PHP: Функции Полезные функции высшего порядка

Библиотека Funct, которую мы рассматривали ранее, кроме обычных функций содержит функции высшего порядка. В этом уроке мы рассмотрим некоторые из них, которые могут быть полезными в будущем.

partition($collection, callable $callback)

Разбивает массив на два на основании предиката. Те элементы, которые удовлетворяют предикату, попадают в первый массив, другие - во второй.

<?php

[$first, $second] = Collection\partition([1, 2, 3, 4, 5, 6, 7, 8, 9], function ($num) {
  return $num % 2 === 0;
}); // => [[2, 4, 6, 8], [1, 3, 5, 7, 9]]

print_r($first); // => [2, 4, 6, 8]
print_r($second); // => [1, 3, 5, 7, 9]

every($collection, callable $callback = null)

Возвращает true если каждый элемент коллекции удовлетворяет предикату. По умолчанию проверяет на истинность ( == true ).

<?php

Collection\every([true, 1, null, 'yes']); // => false
Collection\every([true, 1, 'yes']); // => true
Collection\every(
    [2, 4, 6],
    function ($value) {
        return ($value % 2) === 0;
    }
); // => true

groupBy($collection, callable $callback)

Группирует элементы коллекции в множества на основании ключа, полученного в результате вызова колбека.

<?php

Collection\groupBy([1.3, 2.1, 2.4], function($num) {
    return floor($num);
}); // => [1 => [1.3], 2 => [2.1, 2.4]]

minValue($collection, callable $callback)

Возвращает минимальный элемент коллекции на основании результата переданной функции.

<?php

Collection\minValue(
    [
        10 => [
            'title' => 'a',
            'size'  => 1
        ],
        20 => [
            'title' => 'b',
            'size'  => 2
        ],
        30 => [
            'title' => 'c',
            'size'  => 3
        ]
    ],
    function ($item) {
        return $item['size'];
    }
);
# => Array (
#    'title' => 'a',
#    'size'  => 1
# )

PHP: Функции Замыкание

Начнем урок с небольшого повторения пройденного материала. Каждая функция создает так называемую область видимости. Внутри этой области доступны только те переменные, которые были определены там же. Все что снаружи - по-умолчанию невидимо.

<?php

$age = 5;

function generate()
{
    print_r($age);
}

generate();

Этот код выдаст предупреждение PHP Notice: Undefined variable: age . Переменная $age определена вне контекста функции и невидима внутри. Точно такое же поведение и у анонимных функций.

<?php

$age = 5;

$generate = function () {
    print_r($age);
};

$generate(); // PHP Notice:  Undefined variable: age

Но передача аргументов все же возможна, и делается она благодаря “замыканию”. С помощью этого механизма можно “захватить” переменные из внешнего окружения и использовать их внутри. Правда, только для анонимных функций. Термином “замыкание”, нередко называют и саму функцию, которая осуществила захват внешних переменных.

<?php

$age = 5;

$generate = function () use ($age) {
    print_r($age);
};

$generate(); // 5

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

Может возникнуть закономерный вопрос, зачем это делать, если переменные можно передать в сам вызов.

<?php

$age = 5;

$generate = function ($age) {
    print_r($age);
};

$generate($age); // 5

Замыкания полезны в тех случаях, когда функция определяется в одном месте, а используется в совершенно другом. Замыкание позволяет не таскать за собой гору переменных. А в некоторых ситуациях без них просто не обойтись. Вспомните функцию without из пакета Funct . Эта функция принимает на вход массив и значение, а возвращает новый массив полученный фильтрацией старого по переданному значению. Его реализация, построенная на функциях высшего порядка, подразумевает фильтрацию. Сложность возникает при описании предиката, ведь внутри анонимной функции нужно сравнивать текущее значение и переданный элемент. Замыкание позволяет решить эту задачу просто.

<?php

function without(array $items, $value)
{
    $filtered = array_filter($items, function ($item) use ($value) {
        return $item !== $value;
    });
    // Сбрасываем ключи
    return array_values($filtered);
}

without([3, 4, 10, 4, 'true'], 4); // => [3, 10, 'true']

Без добавления use ($value) ничего не получится. $value не виден внутри анонимной функции.

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

PHP: Функции Парадигмы программирования

В программировании часто используется термин “парадигма”.

Паради́гма программи́рования — это совокупность идей и понятий, определяющих стиль написания компьютерных программ (подход к программированию). Это способ концептуализации, определяющий организацию вычислений и структурирование работы, выполняемой компьютером. (Wikipedia)

Парадигма — это больше, чем просто другой алгоритм решения задачи. Как правило, структура кода при использовании разных парадигм отличается очень значительно и требует знаний, выходящих за рамки только синтаксиса языка (например, автоматное программирование требует хотя бы базового понимания теории автоматов). Причем подавляющее большинство современных (и не очень) языков программирования являются мультипарадигменными и позволяют писать код используя множество стилей. Иногда эти стили взаимоисключающие, иногда они дополняют друг друга. К текущему моменту мы писали код используя две парадигмы: императивную и декларативную.

Императивная парадигма

Императивная парадигма - стиль написания кода в виде набора последовательных инструкций (команд) с активным использованием переменных. Возможно, данное определение звучит страшно, но на практике императивный стиль является доминирующим. Не считая этого курса, весь остальной код мы писали именно в императивном стиле.

<?php

// Поиск максимального числа
$numbers = [10, 20, 52, 105, 56, 89, 96];
$max = $numbers[0];
foreach ($numbers as $number) {
    if ($number > $max) {
        $max = $number;
    }
}
print_r($max); // => 105

В императивном стиле широко используется присваивание (а, значит, и переменные) и циклы. Эта парадигма популярна потому, что она в точности соответствует тому, как работает компьютер: последовательно выполняет инструкции и использует память для хранения промежуточных результатов. Обычно говорят, что императивная программа отвечает на вопрос КАК («как достичь нужного результата»).

PHP как, впрочем, и java/ruby/python/c#/perl/javascript/go относится к императивным языкам. То есть языкам, в которых доминирующей является императивная парадигма (язык толкает к ее использованию). Что, однако, не мешает использовать и другие парадигмы в рамках этих языков.

Декларативная парадигма

Императивному стилю противопоставляют декларативный, который нередко называют функциональным. Ключевое отличие функционального стиля от императивного в том, что при таком стиле программа выглядит как спецификация (которая может быть очень сложной), а не как набор инструкций. То есть программа отвечает на вопрос ЧТО («что мы хотим получить»). Эту грань довольно трудно уловить сразу, но, например, вся математика, по своей сути, декларативна.

<?php

$numbers = [10, 20, 52, 105, 56, 89, 96];
$max = array_reduce($numbers, function ($acc, $number) {
    return $number > $acc ? $number : $acc;
}, $numbers[0]);
print_r($max); // => 105

Главное отличие декларативной парадигмы от императивной на практике - отсутствие присваивания. Вы можете мне возразить, что в коде выше переменные есть и чисто технически это правда. Присмотритесь к коду внимательнее, вы заметите что переменная создана ровно один раз и больше не меняется. По сути, она рассматривается как константа или, если копнуть глубже, как логическое высказывание. В математике это звучало бы так: допустим “A это множество чисел”. Что бы мы дальше не делали, “A” остается всегда тем же чем было во время определения.

Дело в том, что в математике доказательства строятся с помощью логических цепочек. Из одних утверждений следуют другие, и таким образом мы можем прийти к решению задачи. Все это возможно только в том случае, когда утверждения не изменяются. Иначе, следствия могут оказаться неверными уже после того, как они были получены, а значит мы не сможем рассуждать логически.

Тоже самое касается и $acc с $number . Эти параметры всегда определяются ровно один раз, так как каждый вызов функции при таком определении (без использования ссылок) не зависит от другого вызова (спасибо чистым функциям). В мире функциональных языков такую операцию называют связывание . Визуально оно выглядит как присваивание, но это не оно. Попытка связать уже связанный идентификатор (в функциональных языках нет переменных) завершится ошибкой. Ниже пример на языке erlang:

1> A = 4.
4
2> A = 'hey'.
** exception error: no match of right hand side value hey

Отсутствие присваивания автоматически означает то, что в функциональной парадигме невозможно использование циклов. Вместо них используется рекурсия. Другой важной особенностью функционального стиля считается активное использование функций как объектов первого рода. В основном в функциях высшего порядка. Причем они способны заменить рекурсию в подавляющем большинстве задач, в чем мы уже убедились в предыдущих уроках (убедились так, что даже не рассматривали рекурсию). Любая задача из представленных решалась основной тройкой функций высшего порядка.

Функциональных языков довольно много. Они, в своей массе, менее популярны чем императивные, но прочно занимают определенные ниши и активно используются в промышленном программировании. К таким языкам относятся: haskell/erlang/elixir/ocaml/f#. В этих языках нет присваивания и циклов. Императивный код на них написать просто невозможно. Немного особняком стоят такие языки как scala и clojure (и другие из семейства lisp). В этих языках основная парадигма — функциональная, и язык толкает к тому, чтобы писать в таком стиле, но при необходимости на них можно написать самый настоящий императивный код с присваиванием и циклами. А вот, почти все, императивные языки позволяют писать функционально. Причем, если одни языки имеют довольно слабую поддержку функциональной парадигмы, то другие настолько мощную, что в них можно писать только функционально (если хочется). К последним относится и современный JavaScript.

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

Другие парадигмы

Большинство других парадигм являются разновидностями функциональной или императивной парадигм. Из наиболее значимых я бы выделил следующие:

  • Логическое программирование
  • Автоматное программирование
  • Объекто-ориентированное программирование
  • Метапрограммирование

Дополнительные материалы

  1. Парадигмы
  2. Функциональное программирование в PHP

PHP: Функции Абстракция с помощью функций

Главная причина создания функций - повышение уровня абстракции, а не сокращения дублирования кода, как вы могли бы подумать. Второе является следствием первого. И действительно, вместо того чтобы реализовывать сортировку самостоятельно в каждом месте где это требуется, можно воспользоваться функцией sort , которая прячет реализацию внутри себя и позволяет программисту не отвлекаться на ненужные детали (вид сортировки, код для ее выполнения).

В этом заключается суть абстракции, мы игнорируем несущественные стороны, свойства и связи рассматриваемого объекта или процесса, что позволяет представить сложную концепцию в более простой форме. Умение абстрагировать - одно из важнейших свойств нашего мозга. Маленькие дети очень быстро учатся классифицировать предметы нашего мира. Без особого труда понимая, что дерево это дерево, даже если конкретную породу дерева они видят впервые. А два разведенных пальца обозначают зайчика (потому что уши).

Несмотря на это, понимание того, как грамотно строить абстракции (в нашем случае выделять функции) не появляется само по себе. Оно приходит с опытом, при условии, что есть другой человек сильнее вас, который может вам указать на допущенные ошибки, и вы сами отслеживаете проблемы вашей абстракции в процессе эксплуатации (рефлексируете). С другой стороны, излишнее абстрагирование скорее вредно, чем полезно. За награмождением новых сущностей можно потерять суть и тратить больше время на понимание происходящего. Построение абстракции — всегда компромисс. Слишком низкий уровень абстракции приводит к дублированию кода, слишком высокий - может быть очень трудным для понимания (Попробуйте понять Теорию Категорий) и усложняющим решение простых задач.

Функции высшего порядка выводят абстрагирование с помощью функций на новый уровень. Благодаря делегированию поведения внешнему коду (используя анонимные функции), резко расширяется возможность повторного использования кода (алгоритма) в разных ситуациях. И вместо десяти функций для десяти разных участков кода появляется одна функция, которая специфицируется 10 раз разным поведением.

Но не забывайте, что абстракции почти всегда текут.

Пример дырявой абстракции

В первом проекте Хекслета наши ученики совершают одну (совершают много, но сейчас нас интересует одна) ошибку, связанную с неверным выделением абстракций. Если отбросить детали (абстрагироваться!), то задача сводится к написанию функции, которая принимает на вход число, и должна напечатать на экран yes если оно четное и no в обратном случае.

Первое решение выглядит примерно так:

<?php

function check($number)
{
  $result = $number % 2 === 0 ? 'yes' : 'no';
  echo $result;
}

Оно рабочее, но сама концепция четности не выделена в свою абстракцию, что, например, затрудняет тестирование и понимание кода. Здесь он простой, но в более сложных случаях догадаться до того, что за операция выполняется - проблематично. Правильно было бы выделить четность в отдельную функцию (абстракцию).

<?php

function isEven($number)
{
    return $number % 2 === 0 ? 'yes' : 'no';
}

function check($number)
{
  $result = isEven($number);
  echo $result;
}

Посмотрите на код выше внимательно. Все ли с ним нормально?

На самом деле, этот код даже хуже чем первая версия, потому что создана неверная абстракция. Понятие четности числа никак не связано ни с выводом на экран, ни со строчками yes или no . Оно существует в вакууме как математическая концепция и не может знать о том, как её собираются использовать. Я уже не говорю про то, что имя isEven начинается с is , а это значит, что функция - предикат. Такие функции могут возращать только логическое значение и никак иначе (исключений не существует!). Правильный вариант выглядит так:

<?php

function isEven($number)
{
    return $number % 2 === 0;
}

function check($number)
{
  $result = isEven($number) ? 'yes' : 'no';
  echo $result;
}

И это самый примитивный вариант создания абстракций. В реальном коде обычно все значительно сложнее.