Проблема, которую все знают, но с которой мирятся
Представьте:
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 лет какая-то форма семантической проверки имен станет стандартом.
Философская рефлексия
Этот подход возвращает нас к идее семантического программирования. Имя переменной — не просто идентификатор, а контракт с функцией, документация для читателя, а так же ограничение для предотвращения ошибок и интерфейсом для взаимодействия компонентов
Мы привыкли, что компилятор проверяет типы. Почему бы не проверять и смысл? Возможно, будущее программирования лежит именно в этом направлении — где формальные проверки охватывают не только синтаксис и типы, но и семантические соглашения.
Автор: 1QDenisQ
