return-vs-side-effect

Возврат значения против Побочного эффекта.

Пример с Бэтменом и Джокером

Русский | English | Українська

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

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

Джокер, с другой стороны, такой непредсказуемый. Кажется, даже случайный. Он любит делать что-то неожиданное, и всегда оставляет за собой беспорядок. Всё, что Джокер делает, он делает для шоу. И его поступки всегда оставляют след.

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


// Бэтмен, меньше известный как Брюс Уэйн, миллиардер,
// решает вычислить среднюю сумму средств, потраченных им
// на гаджеты и транспортные средства поквартально и за год
const january = 1000;
const february = 2000;
const march = 1200;
const april = 2600;
const may = 3000;
const june = 3100;
const july = 2700;
const august = 2500;
const september = 2300;
const october = 2900;
const november = 2200;
const december = 2100;

// Бэтмен, будучи героем, всегда возвращает результат
function bCalcQuartAvg(month1, month2, month3) {
  return (month1 + month2 + month3) / 3;
}
// всегда возвращает результат!
function bCalcYearAvg(quart1, quart2, quart3, quart4) {
  return (quart1 + quart2 + quart3 + quart4) / 4;
}

// И затем он делает свои вычисления
const q1avg = bCalcQuartAvg(january, february, march);
const q2avg = bCalcQuartAvg(april, may, june);
const q3avg = bCalcQuartAvg(july, august, september);
const q4avg = bCalcQuartAvg(october, november, december);
const yearAvg = bCalcYearAvg(q1avg, q2avg, q3avg, q4avg);

// Мы легко можем увидеть результаты его вычислений
console.log({ q1avg, q2avg, q3avg, q4avg, yearAvg });
// { q1avg: 1400, q2avg: 2900, q3avg: 2500, q4avg: 2400, yearAvg: 2300 }
// и ни одна из его функций не дала побочных эффектов

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

// скажем, Джокер, будучи криминальным гением, решает
// вычислить среднюю сумму денег, которую он и его
// банда крали поквартально и за год, дразня Бэтмена
const january = 1700;
const february = 2200;
const march = 1500;
const april = 2900;
const may = 3100;
const june = 3300;
const july = 2800;
const august = 2600;
const september = 2400;
const october = 3000;
const november = 2300;
const december = 2200;

// Джокер, будучи хаотичным шоуменом, хочет, чтобы все видели
function jCalcQuartAvg(m1, m2, m3) {
  console.log("A-ha-ha!", (m1 + m2 + m3) / 3);
}
// но он ничего не возвращает
function jCalcYearAvg(q1, q2, q3, q4) {
  console.log("BOOM!!", (q1 + q2 + q3 + q4) / 4);
}

// И затем он делает свои вычисления...
const q1avg = jCalcQuartAvg(january, february, march);
const q2avg = jCalcQuartAvg(april, may, june);
const q3avg = jCalcQuartAvg(july, august, september);
const q4avg = jCalcQuartAvg(october, november, december);
// все четыре константы получили значение undefined
const yearAvg = jCalcYearAvg(q1avg, q2avg, q3avg, q4avg);

// Но что мы видим? Не то, что можно было бы ожидать...
// A-ha-ha! 1800
// A-ha-ha! 3100
// A-ha-ha! 2600
// A-ha-ha! 2500
// BOOM!! NaN

Давайте посмотрим, что здесь произошло. Функции Бэтмена возвращали результаты, позволяя нам использовать их в дальнейших вычислениях. Функции Джокера, с другой стороны, просто выводили результаты в консоль и ничего не возвращали. Конечно, мы можем увидеть результаты, но не можем использовать их в дальнейших вычислениях. Да, консоль изменилась, это побочный эффект. Но это и всё. Эти значения, которые мы вывели туда, больше не могут быть использованы. Даже если эта грязная функция все равно вернет значение, это не результат вычисления, а просто undefined. И когда мы попытались использовать эти результаты в дальнейших вычислениях, мы получили NaN (не число) в качестве результата.

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

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

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

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

image