Проблема, которую все знают, но с которой мирятся
Представьте:
auto user_id = get_user_id() //# Хорошо, допустим auto player_id = get_user_id() //# Что? Player? Я думал, это user auto id = get_user_id() //# А это что за id?
Знакомо? Мы тратим ментальные ресурсы на отслеживание: "Как эта функция назвала то, что вернула?", "Как я назвал то, что получил?". А потом в ревью кода:
// Что предпочтительнее использовать в code style? const auto uid = fetchUser(); const auto userId = fetchUser(); const auto user_id = fetchUser(); const auto userIdentifier = fetchUser();
Разнобой. Он замедляет чтение кода, увеличивает когнитивную нагрузку, создает почву для багов.
Рассмотрим концепцию программирования: Строгое семантическоe представление связывания данных
Что если заставить компилятор проверять идентичность имен возвращаемых значений? Не просто типы, а буквальное совпадение:
// Объявляем функцию void get_user_id(int user_id = -1) { user_id = 42 return user_id } // Единственно правильный вызов: auto user_id = get_user_id() // ✓ // Всё остальное - ошибка компиляции: auto userId = get_user_id() // ✗ Регистр auto userID = get_user_id() // ✗ Другой стиль auto id = get_user_id() // ✗ Сокращение auto u_id = get_user_id() // ✗ Аббревиатура
Как это работает?
Функция объявляет, какие имена она возвращает
Вызывающий код должен использовать те же имена
Компилятор проверяет точное совпадение (регистр, подчёркивания, всё)
Что это даёт на практике?
Данный контракт позволит сделать Intellesense умнее, так как мы явно указываем какой контекст мы хотим получить, а автодополнение нам корректно подбирает какой из доступных вариантов функцию мы хотим получить.
Так же это позволяет нам не вызвать функцию 2 раза и одной области видимости, так как имена будут одинаковые, и компилятор/интерпретатор не пропустит такое, так как языки обычно реализовано, что все имена уникальны в одной области видимости.
Еще позволяет проще анализировать контекст использования параметров из разных модулей программы
Интересные следствия
Перегрузка функций через имена возвратов
// Разные функции с одним именем, но разными возвратами: function parse() -> (json_data: JSON) { return json_data; } function parse() -> (xml_data: XML) { return xml_data; } function parse() -> (plain_text: String) { return plain_text; }
Выбор функции определяется тем, что хотим получить:
json_data = parse() // Вызывается JSON-парсер xml_data = parse() // Вызывается XML-парсер plain_text = parse() // Вызывается текстовый парсер
Это меняет парадигму: вы думаете не "какую функцию вызвать", а "что я хочу получить".
Единый кодстайл принудительно
Библиотеки диктуют стиль. Если библиотека использует snake_case, ваш код тоже будет использовать snake_case. Никаких холиваров в команде :)
Особые случаи: Рассмотрим небольшое количество краевых моментов по этому поводу
Возникает вопрос: А как же библиотеки, ведь реализация их недоступна а только API функций, как с этим дела обстоят?
Для разрешения такого конфликта можно рассмотреть "контракты" для возвращаемых аргументов. К примеру реализация через метаданные/атрибуты для нативных библиотек (C/C++):
// Декоратор для экспортируемых функций [[rl_returns(user_id)]] extern "C" int get_user(int* user_id, char* user_name);
Рассмотрим другой пример: А что если функция возвращает не переменную, а объект?
User create_user(){ return User(user_id=42, user_name="John"); }
Отдельно использовать User как имя мы не можем, потому что User это имя типа. Использовать create_user мы не можем, потому что это имя функции.
Первоначальной идеей была делать склеивание имени функции и возвращаемого типа и вышло бы create_user_User, что очень и крайне громоздко и больше выглядит как костыль.
Вариантом так же было использовать примерно такой синтаксис
return construct_user = User(user_id=42, user_name="John");
Но в С++ такая конструкция является недопустимой, если construct_user не определена заранее. В итоге пришел к выводу, что возвращение объекта данных, необходимо явно указывать имя возвращаемого аргумента через контракты.
Краевой случай с вызовом функций:
User create_user(){ return construct_user_profile(); }
Проблема может заключаться в необходимости получения реального значения пользователя из вложенных вызовов функций. Решением будет обеспечение возврата конкретного объекта пользователя из конечной точки цепочки вызовов.
Но тогда возникает другой вопрос: А что если функция является рекурсивной (вызывает сама себя)?
int process_data(){ return process_data(); }
В данном случае, необходимо использовать атрибуты указывающие возвращенное имя, но только в том, случае если функция не возвращает явные имена
[[rl_returns(progress_count)]] int process_data(){ return process_data(); } // или [[rl_returns(progress_count)]] int process_data(){ static int id = 0; static int count = -1; if (count == 0) return id; /* Код */ return process_data(); }
В данном случае квалификатор id будет отброшен, так как атрибуты имеют преимущества
Философская сторона
Этот подход возвращает нас к идее семантического программирования. Имя переменной — не просто идентификатор, а контракт с функцией.
Мы привыкли, что компилятор проверяет типы. Почему бы не проверять и смысл?
Практические перспективы
Данный паттерн будет применяться мной исключительно для подмножества C++, который находится в разработке. Этот DSL(над С++) запрещает проблемные паттерны (общие имена, синонимы), требует явных структур вместо кортежей, и ограничивает область применения (не для мат. вычислений). Хотя даже для кортежей данное правило применяется свободно, но только в данном случае я разбирал конкретно семантику исключительно C++.
Для майнстрим-языков вроде C++ полная реализация маловероятна из-за проблем с обратной совместимостью и потерей гибкости. Однако отдельные аспекты (лучшие практики, статический анализ, улучшенные системы типов) вполне могут быть заимствованы.
Это напоминает историю с const — сначала казалось избыточным ограничением, а теперь без него немыслим современный C++. Возможно, через 10-20 лет какая-то форма семантической проверки имен станет стандартом.
Философская рефлексия
Этот подход возвращает нас к идее семантического программирования. Имя переменной — не просто идентификатор, а контракт с функцией, документация для читателя, а так же ограничение для предотвращения ошибок и интерфейсом для взаимодействия компонентов
Возможно, будущее программирования лежит именно в этом направлении — где формальные проверки охватывают не только синтаксис и типы, но и семантические соглашения.
