Функции пришли в программирование из математики, как естественное понятие, позаимствовав даже запись вида
y = f(x)
И рано или поздно вы столкнётесь с упоминанием чистых функций. Давайте сделаем это сейчас :)
Что это такое?
Чистая функция в программировании это эквивалент функции в математике. Математические функции всегда чистые, но так никогда не назывались, потому что это было не нужно. И только когда функции появились в программировании, пришлось делить их на чистые и нечистые.
Для начала вспомним, что такое математическая функция. Можно сказать, что это какая-то формула, но на самом деле это просто отображение, или соответствие элементов одного множества элементам другого множества:
А вот конкретные правила, которые диктуют, какой элемент должен соответствовать какому, это и есть функция. Она может быть выражена в виде формулы, логических условий, или даже просто в виде таблицы значений. Мы берём элемент a, применяем к нему функцию f и получаем элемент b:
b = f(a)
Особенностью этого соответствия является то, что каждому элементу множества A соответствует только один, и всегда один и тот же, элемент множества B. Такое соответствие всегда однозначно, и это важно.
Чистая функция в программировании это такая функция, которая с одними и теми же параметрами всегда возвращает один и тот же результат, при этом не порождая никаких побочных эффектов ни в каких других местах.
Можно сказать, что большинство программных функций чистые. Например, функция возведения в квадрат, которая определена вот так:
f(x) = x²
и её программный эквивалент (для целых чисел):
Давайте проверим: эта функция при одном и том же x всегда вернёт один и тот же результат; также она больше ничего не меняет и не создаёт никаких побочных эффектов.
Вызов чистой функции с каким-либо параметром можно заменить на возвращаемый ею результат, если этот результат был уже известен. Он всегда будет один и тот же.
Значит, если один раз вычислить sqr(2) и получить результат 4, то потом везде, где встречается sqr(2), можно сразу писать 4, даже не вызывая функцию. Это будет абсолютно равносильно.
А что же тогда нечистые функции?
В противоположность чистым, они не всегда возвращают один и тот же результат для одних и тех же параметров, и/или создают побочные эффекты. Давайте рассмотрим несколько примеров.
1.
Эта функция при одном и том же x будет возвращать разный результат каждую следующую секунду. Она использует нечистую функцию time() и поэтому сама становится нечистой.
2.
Эта функция сама ничего не меняет, но использует глобальную переменную t. Эту переменную может снаружи изменить кто угодно, и тогда результат функции изменится, следовательно она нечистая.
3.
Эта функция хранит собственное состояние в переменной t, не доступной более никому. Но функция меняет это состояние при каждом вызове, и оно влияет на результат. Следовательно, функция нечистая.
4.
Эта функция меняет своё состояние t, но не использует его для результата, и для любого x возвращает x. Это соответствие будет всегда однозначным, и значит, функцию можно назвать чистой?
Да, в некоторых источниках подобную функцию называют чистой, но я могу доказать, что она создаёт побочные эффекты.
Её состояние, хранимое в статической переменной t, никому снаружи не доступно, так как переменная объявлена внутри функции. Следовательно, программа не может обратиться к этому состоянию, его всё равно что не существует.
Однако статическая переменная t всё равно находится в памяти вместе с другими статическими переменными. В языке C мы можем создать глобальную статическую переменную test и предположить, что t, принадлежащая функции, находится где-то рядом с ней. Используя арифметику указателей, мы можем найти её по содержимому DEADBEEF:
Следовательно, в данном случае функция, изменяющая своё состояние, создаёт побочный эффект, влияющий на выполнение программы.
Поэтому, если функция изменяет любую память, за исключением собственного стека, она уже не может считаться чистой.
Если уж на то пошло, мы можем через указатели и в стек залезть и посмотреть, чего там наизменяла функция, и тоже считать это побочным эффектом, но тогда чистых функций не останется совсем.
5.
Здесь в функцию передаётся указатель. Передавая один и тот же указатель, мы не можем быть уверены в том, что за значение находится по этому указателю, так что результат работы функции непредсказуем, и следовательно она нечистая. Однако одни и те же значения по указателю будут давать один и тот же результат, так что с этой точки зрения функция чистая и не имеет побочных эффектов.
6.
Эта функция всегда возвращает 0, поэтому её можно было бы считать чистой. Её вызов можно просто заменить на 0. Но она модифицирует значение, переданное по указателю, и значит имеет побочные эффекты.
7.
Эта функция возвращает однозначный результат и не меняет ничего в памяти программы, но меняет состояние потока ввода-вывода, используя функцию printf(), что является побочным эффектом (и нарушением первого принципа SOLID).
Чистые функции лучше
Как можно было видеть выше, есть довольно много способов сделать функцию нечистой и тем самым выстрелить себе в ногу. Непредсказуемость результата и побочные эффекты будут приводить к багам в программе вплоть до совершенно необъяснимых вещей.
В то же время чистые функции предсказуемы, однозначны и не создают побочных эффектов. Это даже позволяет компилятору их оптимизировать.
К примеру, такая функция:
Если в программе написать:
y = increment(5);
То компилятор может поступить следующим образом. Во-первых, вызов функции он заменит на прямую вставку её кода:
y = 5 + 1;
Далее он увидит, что это можно оптимизировать, и заменит на:
y = 6;
Это, конечно, вырожденный и очень утрированный пример, потому что 6 мы могли бы и руками сразу написать. И не факт, что компилятор вообще будет этим заниматься. Но сама возможность заменить функцию на её значение появляется именно потому, что функция чистая.
Чистые функции безопасны для использования в мультипоточной среде, так как не хранят и не изменяют никакие состояния.
Иногда, или даже часто, у вас в коде возникают проблемы, для которых напрашивается решение в виде нечистых функций. Передавать туда указатели для изменения, например. Но это как раз шанс подумать и переделать код так, чтобы все функции были чистые. Тогда, возможно, и сам код откроется вам с какой-то другой стороны.
P.S.
Но понятно, что не все функции могут быть чистыми. Например, rand(), возвращающая случайное значение, просто-таки обязана быть нечистой.