Найти тему
Журнал «Код»

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

Хороший программист старается делать свои функции чистыми. Если знать, что это такое, можно сойти за своего, а заодно написать читаемый код.

Что такое функция

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

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

Допустим, в игре есть 100 ситуаций, когда нужно добавить или убавить очки — для каждого типа врага, преграды, уровня и т. д. Чтобы в каждой из ста ситуаций не писать одни и те же восемь строк кода, вы упаковываете эти восемь строк в функцию. И теперь в ста местах вы пишете одну строку: например, changeScore(10) или changeScore(-10). В первом случае прибавится 10 очков, во втором — столько же отнимется, но функция используется одна.

Если теперь изменить, что происходит в функции changeScore(), то изменения отразятся как бы во всех ста местах, где эта функция вызывается.

Зачем нужны функции

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

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

А что если нужно не только писать очки в файл, но и следить за рекордом? Пишем новую функцию getHighScore(), которая достаёт откуда-то рекорд по игре, и две другие — setHighScore() и celebrateHighScore() — одна будет перезаписывать рекорд, если мы его побили, а вторая — как-то поздравлять пользователя с рекордом.

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

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

Без функций трудно повесить действия на какие-либо кнопки в интерфейсе. Например, у вас на сайте есть форма, и при клике на кнопку «Отправить» вы хотите проверять, что данные в форме правильно введены. Вы спокойно описываете где-то в коде функцию validateForm() и вешаете её на нажатие кнопки. Кнопку нажали — функция вызвалась. Не нужно вписывать в кнопку полный текст программы.

А без функции пришлось бы писать огромную программу-валидатор прямо внутри кнопки. Это исполнимо, но код выглядел бы страшно громоздким. Что если у вас на странице три формы, и каждую нужно валидировать?

Хорошо написанные функции резко повышают читаемость кода. Мы можем читать чужую программу, увидеть там функцию getExamScore(username) и знать, что последняя каким-то образом выясняет результаты экзамена по такому-то юзернейму. Как она там устроена внутри, куда обращается и что использует — вам неважно. Для нас это как бы одна простая понятная команда.

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

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

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

В общем, функции — это бесконечная радость. На этом наш экскурс в функции закончен, переходим к чистоте.

Что такое чистые функции

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

Вот примеры.

Один и тот же результат

Допустим, мы придумали функцию, которая считает площадь круга по его радиусу: getCircleArea(). Для наших целей мы берём число пи, равное 3,1415, и вписываем в функцию:

Теперь этой функции надо скормить число, и она выдаст площадь круга:

getCircleArea(2) всегда выдаст результат 12,6060
getCircleArea(4) всегда выдаст 50,2640

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

Другой пример. Мы пишем программу-таймер, которая должна издать звук, например, за 10 секунд до конца отведённого ей времени. Чтобы узнать, сколько осталось секунд, нам нужна функция: она выясняет количество секунд между двумя отметками времени. Мы даём ей два времени в каком-то формате, а функция сама неким образом высчитывает, сколько между ними секунд. Как именно она это считает, сейчас неважно. Важно, что она это делает одинаково. Это тоже функция с предсказуемым результатом:

getInterval('09:00:00', '09:00:12') всегда выдаст 12
getInterval('09:00:00', '21:00:00') всегда выдаст 43 200

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

getSecondsTo('23:59:59') в один момент даст 43 293 секунды,
а спустя 2 минуты эта же функция getSecondsTo('23:59:59') даст 43 173 секунды.

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

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

var now = getCurrentTime();
var interval = getInterval(now, '23:59:59');

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

Побочные эффекты

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

Примерчики!

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

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

getTasksByPriority(‘high’) — вернёт новый массив с приоритетными задачами, не изменив другие массивы. В памяти был один массив, а теперь появится ещё и второй.

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

Программисты настороженно относятся к мутациям, потому что за ними сложно следить. Что если из-за какой-то ошибки функции выполнятся в неправильном порядке и уничтожат важные для программы данные? Или функция выполнится непредсказуемо много раз? Или она застрянет в цикле и из-за мутаций разорвёт память? Или мутация произойдёт не с тем куском программы, который мы изначально хотели?

Вот типичная ошибка, связанная с мутацией. Мы пишем игру, нужно поменять сумму игровых очков. За это отвечает функция changeScore(), которая записывает результат в глобальную переменную playerScore — то есть мутирует эту переменную. Мы случайно, по невнимательности, вызвали эту функцию в двух местах вместо одного, и баллы увеличиваются вдвое. Это баг.

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

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

Как этим пользоваться

Когда будете писать свою следующую функцию, задайтесь вопросами:

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

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

Подписывайтесь на наш канал, и да пребудут с вами логика и структура!