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

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

Функция Math.random() возвращает случайное число от 0 до 1:

Math.random(); // 0.9337432365797949
Math.random(); // 0.5550694016887598

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

Например, недетерминированными являются функции, оперирующие системным временем. Так, функция Date.now() каждый раз возвращает новое значение:

// Возвращает текущие время в миллисекундах
Date.now(); // 1571909874844
Date.now(); // 1571909876648

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

getYear(); // 2019

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

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

console.log('lala');

Вызов console.log('lala') выполнил два действия:

  • Вывел сообщение lala в терминал (или консоль браузера, в зависимости от среды выполнения)
  • Вернул значение undefined. Какое сообщение бы мы не печатали, возвращаемое значение всегда будет одно — undefined.

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

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

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

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

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

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

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

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

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

const getCurrentShell = () => process.env.SHELL;

getCurrentShell(); // /bin/bash

Функция getCurrentShell обращается к переменной окружения SHELL. Но в разные моменты времени и в разных окружениях значение этой переменой может быть различным.

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

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

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

const sayHiTo = (name) => {
  const greeting = `Hi, ${name}!`;
  console.log(greeting);
};

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

const sum = (num1, num2) => {
  const result = num1 + num2;
  return result;
};

// далее, там где можно, будем демонстрировать вариант однострочника, например:
// const sum = (num1, num2) => num1 + num2;

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

console.log(sum(4, 11)); // => 15

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

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

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

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

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

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

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

Pure
Functions

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

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

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

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

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


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

  1. Побочные эффекты
  2. Детерминированная функция
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →