Процедуры и функции - это то, чему любой разработчик учится ещё в самом начале своего разработческого пути. Если вы не видите разницы, просто знайте, что процедура - не возвращает никакого значения, а функция - возвращает. Такая незначительная разница. Она была явно указана для языка Pascal, а вот для C++ - уже нет. Но это не важно, сегодня мы поговорим о функции, как о квинтэссенции разработки.
Как работает функция
Вообще, функция - это всего лишь манёвр при разработке программного обеспечения. На уровне процессоров никаких функций не существует, есть лишь непрерывный поток команд и прерываний (или системных вызовов), однако сама возможность использования функции заложена именно в архитектуре железа.
Как вызвать функцию? Очень просто (или не просто): в общем виде, в стек сохраняются данные, которые будут аргументами для функции и текущий адрес команды, затем управление передаётся на некоторый иной адрес, который извлекает из стека параметры, обрабатывает их и возвращает результат, например, через регистр общего назначения (но тут много вариантов).
Если ничего не поняли - не важно, важно, что изначально функция - это лишь логическая надстройка над существующим порядком вещей. И, надо сказать, очень элегантная надстройка.
В чём суть функции
Напишу банальность (которую хорошо бы осмыслить ещё раз), но
любая функция получает на вход некоторый набор данных, затем проводит некоторые преобразования или операции, после чего возвращает некоторый результат
Если мы заменим слово "функция" на "компонент" или "программа", но высказывание останется верным. Проще сказать, функция, то, как она была задумана - является той самой высшей формой эволюции любого программного компонента, независимо от его формы и размера. Да простится мне сей пафос.
Многопоточностью и асинхронность
Вообще, функция - это что-то про линейное выполнение кода, а что делать, когда линейности нет? По правде говоря, тут ничего не изменилось
Операционная система выдаёт каждому потоку свой квант времени на работу, затем передаёт процессор в пользование другому. Просто программа более высокого уровня в цикле запускает и останавливает выполнение своих подпрограмм. Функция - цикл - функция. Плюс повышенный набор возможностей и привилегий.
Асинхронность, по сути, мало чем отличается от многопоточности - то же цикл, только не на уровне операционной системы, а на уровне приложения. Вместо потоков - карусель вызовов. А вот сколько будет работать каждый вызов - остаётся на усмотрение разработчика. Опять: функция - цикл - функция. На этот раз без дополнительных привилегий.
Мыслить категориями функции
Короче говоря, функция - это абстракция, которая описывает как самый минимальный, так, теоретически, и любой другой отчуждаемый модуль некоторой отдельной функциональности.
Если рассматривать любую решаемую задачу, как ещё одну функцию, то сразу всё меняется:
- Точка входа - набор данных, точка выхода - набор данных;
- Все "внутренние" действия могут быть поделены на вызовы других функций внутреннего функционала;
- Результатом выполнения должен стать некоторый выходной набор данных.
Всё просто. Но как с этим работать?
Первые трудности
Хорошо, давайте попробуем написать веб-приложение, которое поддерживает указанный выше тезис, где всё - функция.
Изначально загружаются данные, загружаются асинхронно и вообще это никак не управляется, т.к. отрабатывается на внутренних механизмах браузера.
Затем надо авторизоваться. Или не авторизовываться и перейти в открытую часть приложения. Какая функция тут может быть вызвана, чтобы определить, где находиться?
А что со строкой URL? Как с ней работать - ведь она, изначально, указывает какой-то ресурс, который должен быть загружен, что, порой, может идти вопреки обработчику функции.
И набор таких вопросов можно продолжать. А знаете почему? Потому что есть простой и понятный подход, который изначально был заложен в контексте веб-разработки (грузится страница, грузятся скрипты, потом скрипты начинают выполняться, частично или полностью переписывая страницу, при обработке страница может быть перенаправлена на другой адрес итд), а основанный на функции подход предполагает некоторую централизацию такого управления.
И на уровне браузера она есть, но она идёт отдельно от программы, позволяя получать доступ к каким-то ключевым явлениям через обработчики событий. И это немного усложняет процесс, т.к. на одном событии надо принимать одни решения, на втором - другие. Не получается никак сделать единую точку входа, с едиными данными.
Выход есть
Вообще, попытка осмыслить работу приложения через единую функцию требует некоторого мастерства, особенно, если к этому отсутствует привычка. При разработке, скажем, на C++ или на Rust - всегда есть некоторая функция main, которая является точкой входа в приложение, но что делать, если такая точка, в принципе, не предусмотрена (как в примере выше)?
Тут придётся немного изменить привычный подход. Скажем, выходными данными могут стать объект строки URL, параметры хранилища и куки; обработчики состояний - внутренние функции, обрабатывающие события, а выходными данными - Promise с управляющим набором данных (куда переадресовываться, какие запросы надо вызвать, какие файлы дополнительно подгружать итд).
В чём главное преимущество
Хотя подход, о котором я пишу, понятен любому разработчику, а перечисленные доводы должны объяснить как этим пользоваться, остаётся один важный вопрос: а мне это зачем? Ну как-то ведь я работал раньше, соблюдал некоторые правила, неплохо справлялся. Зачем мне пересматривать свой подход?
Ответ прост: ради не очевидных, но невероятно полезных улучшений программы. Что будет, если начать использовать такой подход:
- Локализация: за каждую функциональность отвечает своя функция или цепочка функций, объединённых под одним именем;
- Всегда понятные входные и выходные данные: мы знаем, что отдаём и что получаем (даже если это что-то - ещё одна функция);
- Возможность разнесения по файлам без ущерба для использования: импортировать одну функцию и использовать её намного проще, чем импортировать целый пакет и использовать их в разных местах;
- Вынужденная оптимизация кодовой базы под новые правила: размазанная по нескольким областям функциональность собирается в одном месте, что повышает управляемость.
Можно перечислить ещё несколько примеров выгод, но главная заключается в том, что использования функция-подхода - это краеугольный камень разработки; наиболее эффективная абстракция, которая только может существовать при текущем оборудовании (возможно, впоследствии это изменится).
Опыт использования
Данный подход я использую уже довольно давно. Выглядит это так: есть очень большой и очень старый проект, внутри которого, как слои при раскопках, встречаются то одни, то другие технологии. И часто это старое барахло надо перетряхивать и менять. Тут и возникает сложность: либо "вписать" новое в старое (это часто возможно, но не всегда просто), либо использовать предложенный подход: не всегда, но часто, я использую именно его.
Смысл простой: в качестве аргументов функции передаётся минимальный контекст выполнения задачи (контекст, безусловно, всегда шире, т.к. есть некоторые "более глобальные" данные, доступ к которым есть и так).
В рамках явного и неявного контекстов, производится обработка данных, принятие решений и, при необходимости, анализ данных. А на выходе - появляются те данные, на основании которых должна быть произведена явная модификация.
Хотя, признаюсь, кое-что меняется внутри и не явно, но в контексте выполняемых задач это, в принципе, допустимо.
Вместо заключения
Главный вопрос: надо ли это вам? Осознаёте ли вы всю изящность и величия функции, либо же считаете, что я восхищаюсь чрезмерно - дело ваше. Лично мне всегда хочется, чтобы код выстраивался понятно, логично и иерархично, но, опыт показывает, что это бывает очень не часто: всегда появляется что-то, что заставляет пойти иным путём, "дать слабину", пожертвовать качеством во имя сроков, добавив пару человеко-лет в технический долг, который никто никогда не закроет. Но зато есть, к чему хочется стремиться.