Функции в программировании обладают рядом важных характеристик. Зная и понимая эти свойства, вы сможете эффективнее строить работу с ними и научитесь правильно их выделять из кода.
Детерминированность
Начнем со стандартной функции rand
. Если вызвать ее без аргументов, она возвращает некоторое случайное число:
<?php
rand(); // 151273074
rand(); // 1129177627
Эта функция нужная и полезная, но неудобная в отладке и тестировании. Связано это с тем, что для одних и тех же входных аргументов, она может возвращать разные значения. То же самое происходит при отсутствии аргументов. Функции с таким поведением называются недетерминированными. Например, к ним относятся функции, оперирующие системным временем:
<?php
// Возвращает текущий год
date('Y'); // 2024
Прямо сейчас повторный запуск вернет точно такое же значение, но через год оно уже будет другим (2025). Другими словами, функция считается недетерминированной в том случае, когда она хотя бы изредка выдает разные значения при одинаковых входных данных.
Есть и противоположные функции — детерминированные. Для одних и тех же входных данных они всегда выдают один и тот же результат. Яркий пример — функции в математике. Для одинакового значения x
результат работы функции y = f(x)
будет одинаковый.
Это не очевидно, но функция print_r
тоже детерминированная:
<?php
var_dump(print_r('lala'));
bool(true)
Дело в том, что она всегда возвращает значение true
для любых входных данных. Можно было бы подумать, что она возвращает то, что печатается на экран, но это не так. Печать — это побочный эффект, о котором мы поговорим чуть позже.
Детерминированность не ограничивается программированием или математикой. Сквозь него можно рассматривать практически любой процесс. Например, подбрасывание монетки — это недетерминированный процесс, его результат всегда случаен.
Почему это понятие так важно? Детерминированность дает предсказуемость. Это напрямую влияет на количество состояний, которые надо обрабатывать и предусматривать. В итоге код становится проще, а логика — прямолинейнее.
Для примера рассмотрим создание чего-либо на сайтах в интернете. Обычно это недетерминированный процесс. Если дважды быстро нажать на кнопку отправки формы, то во многих ситуациях получится два оставленных комментария или два отправленных платежа. Чтобы таких проблем не было, разработчики сайтов ставят защиту от двойных нажатий или проводят проверку на дубликаты уже внутри приложения.
Другой пример — создание директории. В командной строке эта операция выполняется с помощью программы mkdir
:
mkdir test
mkdir test
mkdir: test: File exists
Как видно из вывода выше, повторный запуск создания директории выдает ошибку — она уже создана. Получается, нам нужно учитывать два состояния:
- Директории не существовало
- Директория существует
В случае детерминированной версии mkdir
об этом можно было бы не думать.
Вообще детерминированность играет огромную роль в администрировании, особенно в задачах, связанных с программной настройкой серверов, выкладкой ПО и обновлениями. Вы можете самостоятельно изучить этот вопрос, поискав по ключевым словам docker, immutable infrastructure и ansible.
Функция становится недетерминированной еще и в одном случае: если она обращается не только к своим аргументам, но и к глобальным переменным, переменным окружения и другим внешним данным. Так происходит, потому что внешние данные могут измениться. Тогда функция начнет выдавать другой результат, даже если в нее передаются одни и те же аргументы:
<?php
function getCurrentShell()
{
// Функция getenv обращается к указанной переменной окружения
return getenv('SHELL'); // /bin/bash
}
В общем случае нельзя сказать, что не детерминированные функции — абсолютное зло. Многих полезных программ не существовало бы без недетерминированного кода. Тем не менее общие рекомендации при работе с детерминированностью звучат так:
- Пишите детерминированные функции всегда, когда есть возможность
- Не используйте глобальные переменные
- Создавайте функции, зависящие только от своих аргументов
Побочные эффекты
Вторая ключевая характеристика функций — побочные эффекты. Так называют любые действия, изменяющие среду выполнения. К ним относятся запись в файл, отправка или прием данных по сети, вывод в консоль, чтение файла и другие файловые операции. Кроме того, побочными эффектами считаются:
- Обращения к глобальным переменным на чтение и запись
- Изменение входных аргументов, когда они передаются по ссылке
Вызов функции с побочными эффектами также считается побочным эффектом:
<?php
function sayHiTo($name)
{
print_r("Hi, {$name}");
}
С другой стороны, любые вычислительные операции не считаются побочными эффектами. Для примера возьмем функцию, которая суммирует два числовых аргумента:
<?php
function sum($num1, $num2)
{
return $num1 + $num2;
}
Без побочных эффектов невозможно написать ни одной полезной программы. Какие бы важные вычисления она ни делала, их результат должен быть как-то продемонстрирован. В самом простом случае его нужно вывести на экран, что автоматически приводит нас к побочным эффектам:
<?php
print_r(2 ** 5);
Побочные эффекты составляют одну из самых больших сложностей в разработке, тестировании и отладке. Они приводят к ошибкам. Например, при одной только работе с файлами количество возможных ошибок измеряется сотней — закончилось место на диске, нельзя прочитать данные из несуществующего файла. Для предотвращения таких ошибок код обрастает множеством проверок и защитных механизмов.
Невозможно совсем избавиться от побочных эффектов, но можно минимизировать их влияние на программу. Как правило, в типичной программе побочных эффектов не так много, и происходят они лишь в начале и в конце.
Например, если программа конвертирует файл из текстового формата в PDF, в идеальном случае она выполняет ровно два побочных эффекта:
- В начале работы читает файл
- В конце работы записывает результат в новый файл
Между этими двумя пунктами и происходит основная работа, которая содержит чистую алгоритмическую часть. Поэтому побочные эффекты будут находиться только в верхнем слое приложения. Ядро, выполняющее основную работу, останется чистым от них.
Инкремент и декремент — единственные базовые арифметические операции в PHP, которые обладают побочными эффектами (изменяют само значение в переменной). Именно поэтому с ними сложно работать в составных выражениях. Иногда они порождают ошибки, которые настолько сложно отловить, что во многих языках вообще отказались от них. Например, в Ruby и Python их нет, а в JavaScript стандарты кодирования советуют их не использовать.
Чистые функции
Идеальная функция с точки зрения удобства работы с ней называется чистой. Чистая функция — это детерминированная функция, которая не производит побочных эффектов. Она зависит только от своих входных аргументов и всегда ведет себя предсказуемо. Такие функции на 100% соответствуют своим математическим аналогам и могут рассматриваться как математические функции.
Чистые функции обладают рядом ключевых достоинств:
- Их просто тестировать — достаточно передать на вход функции нужные параметры и посмотреть ожидаемый выход
- Их безопасно запускать повторно, что особенно актуально в асинхронном или многопоточном коде
- Их легко комбинировать, получая новое поведение без необходимости переписывать программу
В хорошо спроектированных программах побочные эффекты изолированы в небольшой части приложения так, чтобы большая часть кода была чистой.
Сейчас это может звучать абстрактно, но ничего страшного в этом нет. Осознание этой темы требует набитых шишек, связанных со сложностью работы в мешанине побочных эффектов. Тему чистоты мы будем поднимать регулярно. Особенно сильно она прорабатывается в реальных проектах.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.