Проблема, которую все знают, но с которой мирятся

Представьте:

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()       // ✗ Аббревиатура

Как это работает?

  1. Функция объявляет, какие имена она возвращает

  2. Вызывающий код должен использовать те же имена

  3. Компилятор проверяет точное совпадение (регистр, подчёркивания, всё)

Что это даёт на практике?

Данный контракт позволит сделать 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 лет какая-то форма семантической проверки имен станет стандартом.

Философская рефлексия

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

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

Комментарии (38)


  1. mosinnik
    02.02.2026 13:08

    Интересная мысль делать сверку имен в рамках правил сонар куба например.

    Но много краевых проблем когда будет куча варнингов по именовани или надо сильно усложнять код. Например случай когда ищем пару объектов из базы и потом с ними чтото делаем - оба должны быть в одном контексте для работы и как они тогда должны именоваться? Туда же чтото более сложное по физ/мат вычислениям где имя подфункции и название переменной связаны условно в рамках формул.


    1. 1QDenisQ Автор
      02.02.2026 13:08

      Вы правы для общего языка. Но данный паттерн будет применять исключительно для подмножества C++, который находится в разработке. Этот DSL(над С++) запрещает проблемные паттерны (общие имена, синонимы), требует явных структур вместо кортежей, и ограничивает область применения (не для мат. вычислений). Хотя даже для кортежей данное правило применяется свободно, но только в данном случае я разбирал конкретно семантику исключительно C++.

      Вот как звучит правило для картежей:

      Для картежей правило наименование становится тем же, и применяется к каждому аргументу

      sender, recipient = find_sender_and_recipient()
      
      # Придётся писать:
      def find_sender_and_recipient() {
          user1, user2 = find_users()  # внутри можно
          sender = user1
          recipient = user2
          return sender, recipient
      }

      В данном случае sender, recipient и return sender, recipient совпадают. Правило применяются. Если вы имели ввиду что-то другое, уточните вопрос :)


      1. mosinnik
        02.02.2026 13:08

        вот этот момент, что это идеи для самостоятельного DSL лучше было бы в статью как раз внести, теперь стало понятнее.

        пример с кортежами тоже ясен, как раз это имелось в виду под усложнением кода, т.к. требуется много бойлерплейта для упаковки/распаковки, чтобы все имело имена.

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


        1. 1QDenisQ Автор
          02.02.2026 13:08

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


        1. 1QDenisQ Автор
          02.02.2026 13:08

          вот этот момент, что это идеи для самостоятельного DSL лучше было бы в статью как раз внести, теперь стало понятнее.

          Если я не ошибаюсь, то это есть в статье:

          Данная идея интересна как исследовательская концепция и могла бы быть реализована в:

          • Доменно-специфичных языках (DSL) — где строгость именования критически важна

          • Образовательных языках для обучения строгому стилю программирования

          • Экспериментальных языках


          1. mosinnik
            02.02.2026 13:08

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


            1. 1QDenisQ Автор
              02.02.2026 13:08

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


    1. 1QDenisQ Автор
      02.02.2026 13:08

      В моём DSL варнинги по именованию — это часть языка. Система заставляет явно описывать семантику через имена. Если получается 'куча варнингов' — значит, в коде было 'куча неявностей'.


  1. Conung_ViC
    02.02.2026 13:08

    так.
    У меня 2 юзера и я хочу сравнить их айди.

    // Единственно правильный вызов:
    auto user_id = get_user_id(user1) // ✓

    а второй как?
    auto user_id2 = get_user_id(user2) // нарушит твою конвенцию имен

    А если он не юзер, а админ, и я хочу это подчеркнуть?


    1. 1QDenisQ Автор
      02.02.2026 13:08

      Можно использовать кортежи: К примеру

      auto [user1_id, user2_id] = get_user_ids(user1, user2);  // Функция возвращает кортеж

      Правило остаётся, но есть контролируемые исключения для реальных сценариев.


      1. Woodroof
        02.02.2026 13:08

        И мы усложняем код только для того, чтобы переменные были фиксированные. Убрали сложность в одном месте, но добавили в другом (и, ИМХО, больше).


      1. kipar
        02.02.2026 13:08

        а что если надо так:

        auto cur_user_id = get_user_id(get_cur_user());
        auto cur_group = find_group_for_id(cur_user_id);
        auto creator_id = get_user_id(cur_group.creator);
        auto creators_group = find_group_for_id(creator_id);
        for(auto other_user: cur_group.users)
        { 
          other_id = get_user_id(other_user);
          ...//далее в коде используются все предыдущие id, например:
          if (other_id == cur_user_id)
            continue;
          ...
        


        1. tenzink
          02.02.2026 13:08

          Есть варианты:

          <sarcasm on>

          • С++-style преобразования name_cast

          auto cur_user_id = name_cast<cur_user_id>(get_user_id(get_cur_user()));
          • Дополнительные функции для преобразования:

          auto get_cur_user_id(auto user_id) { ... }
          auto cur_user_id = get_cur_user_id(get_user_id(get_cur_user()));
          • Введение неявных преобразований имён:

          user_id может быть преобразован в

          1. <прилагательное>_user_id

          2. <числительное>_user_id

          то есть разрешены конверсии user_id => current_user_id, second_user_id, other_user_id, power_user_id

          а такие конверсии запрещены user_id => price_id, mail_and_user_id

          <sarcasm off>


          1. 1QDenisQ Автор
            02.02.2026 13:08

            Очень интересная задумка


          1. kipar
            02.02.2026 13:08

            user_id может быть преобразован в
            <прилагательное>_user_id

            получается что-то типа венгерской нотации. Хорошо что статическая типизация избавила нас от необходимости хранить в каждом имени переменной еще и тип.


            1. tenzink
              02.02.2026 13:08

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


  1. includedlibrary
    02.02.2026 13:08

    Допустим я всё именую в snake_case, а автор очень нужной мне библиотеки - в camelCase, а остальные библиотеки тоже используют snake_case, как предполагается такие проблемы решать?


    1. 1QDenisQ Автор
      02.02.2026 13:08

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


      1. includedlibrary
        02.02.2026 13:08

        Просто это довольно странно. Нужно либо всех обязать использовать какой-то один регистр, либо дать возможность переопределить имена. Иначе, очень вероятно, что люди просто забьют и будут использовать библиотеку с camelCase именами в проекте с преимущественно snake_case именами. Обычно даже строгих проверок делать не надо, достаточно просто конвенции прописать. В тех же ruby и go чаще соблюдают snake_case и camelCase соответственно, хотя ни интерпретатор в случае ruby, ни компилятор в случае go никак этому не препятсвует.


        1. 1QDenisQ Автор
          02.02.2026 13:08

          Абсолютно с вами согласен. Но я решил на данный момент отталкиваться от абсолютной строгости. Я не решил вводить "полумеры" в начале пути, потому что пришлось бы привести овер дофига пример/контраргументов. Эти полумеры в начале пути я посчитал избыточными. Я не против развить эту идею. Данная концепция не приватизирована каким-либо образом


        1. 1QDenisQ Автор
          02.02.2026 13:08

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


          1. includedlibrary
            02.02.2026 13:08

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


    1. Deosis
      02.02.2026 13:08

      Единый кодстайл принудительно

      Как быть если совпадут имена результатов из разных функций? Нельзя их вызывать из одного места.
      Или высокий уровень сложности: имя результата одной библиотеки совпадает с типом из другой.

      А можно пойти ещё дальше и требовать точного совпадения ещё и аргументов функций, тогда не получится перепутать ширину и высоту. Правда, половина кода уйдет на переименование переменных и жонглирование областями видимости: const auto& arg = result; повсюду


      1. 1QDenisQ Автор
        02.02.2026 13:08

        и требовать точного совпадения ещё и аргументов функций

        Это тоже планируется ввести

        Правда, половина кода уйдет на переименование переменных и жонглирование областями видимости: const auto& arg = result; повсюду

        Не придется, так как для существующих языков это сломает обратную совместимость. А для новых - принудильный стиль программирования


  1. tenzink
    02.02.2026 13:08

    Вот уж, придумать несуществующую проблему, а потом её криво "решить".

    Если прям хочется, настройте LLM в code review для проверки семантики имён переменных


    1. 1QDenisQ Автор
      02.02.2026 13:08

      Проблема существует в legacy-проектах с разросшейся кодовой базой. LLM — хороший костыль, но они работают с уже существующим разнобоем. Предлагаемый подход — попытка предотвратить разнобой на системном уровне. Мое предложение — не замена код-ревью, а формализация соглашений. Так же как const формализует неизменяемость, а типы — структуру данных. Не для всех проектов, но для некоторых критически важных систем — возможно, оправданно.


      1. tenzink
        02.02.2026 13:08

        legacy-проектам ваш язык никак не поможет. Да и проблемы таких проектов бесконечно далеки от неудачного выбора имён


        1. 1QDenisQ Автор
          02.02.2026 13:08

          legacy-проектам ваш язык никак не поможет. 

          Я сказал что существует проблема в легаси-проектах. Читайте внимательно, пожалуйста.

          Да и проблемы таких проектов бесконечно далеки от неудачного выбора имён

          Вы недооцениваете проблему, потому что рассматриваете её в парадигме «стиль кода». На деле это проблема семантической целостности распределённой кодовой базы, которая в legacy проявляется особенно остро. LLM — паллиатив, который работает с симптомами. Мой подход — попытка лечения причины через формализацию контрактов. Вы говорите: "Зачем строгие имена, если есть LLM?" это тоже самое что как говорить в 1995 году: "Зачем нам статическая типизация в Java, если есть хорошие тестировщики?"

          Вы знаете, есть интересный парадокс в нашей индустрии. Те, кто начинал в 90-е (и я в этом уверен, у вас богатый опыт), выработали железобетонный рефлекс: "Если работает — не трогай". И именно поэтому вы так скептически смотрите на предложения "всё переделать по-новому". Потому что видели, как "новые правильные подходы" разбивались о реальность дедлайнов, ограниченной памяти и процессоров, которые сегодня кажутся игрушечными.

          То, что было вынужденной необходимостью в вашу эпоху - сегодня стало культурой технического долга.

          Вы смотрите на моё предложение и видите: "Ещё одна умная идея, которая разобьётся о реальные проекты". Потому что ваша реальность 20 лет назад — это C++ без STL, без RAII, где каждая лишняя проверка — это просадка производительности.

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


          1. tenzink
            02.02.2026 13:08

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

            Так что желаю вам удачи и творческих успехов.


            1. 1QDenisQ Автор
              02.02.2026 13:08

              Вы начали с тезиса "несуществующая проблема". Когда я показал, что проблема существует в крупных системах, вы решили закончить диалог.

              Теперь, когда технические контраргументы закончились, вы прячетесь за "неинтересно обсуждать".

              Дискуссия закончена не потому, что проблема несущественна, а потому, что вы предпочли защищать своё эго вместо анализа аргументов.

              Жаль. Настоящая инженерия начинается там, где заканчивается догматизм. Удачи вам в рамках вашей картины мира.


  1. Siemargl
    02.02.2026 13:08

    1. Различные сущности выделять в различные типы.

    2. Использовать аннотированные (именованные) параметры функций.

    Но С++ для этого не очень подходит - аннотирование отсутствует (можно чуть обойти в С++20), отсутствует перегрузка по возвращаемому значению, слишком легкое автоприведение типов.


  1. muhachev
    02.02.2026 13:08

    оверинж нейроманьяка, который не только сам не умеет писать нормальный код, так ещё и пытается усложнить жизнь другим.


    1. 1QDenisQ Автор
      02.02.2026 13:08

      Могли бы аргументировать выражение "сам не умеет писать нормальный код, так ещё и пытается усложнить жизнь другим"?


  1. manyakRus
    02.02.2026 13:08

    1) Начинать надо с правильных названий функций, это ещё важнее:
    чтоб в названии функции был возвращаемый тип,
    типа:
    FindUser()
    CreateUser()
    FindUserAndGroup()
    ещё лучше:
    Find_User() с подчёркиванием т.к. таких Find итак уже сотни, но некоторые не рекоммендуют смешивать стили snake_case и CamelCase

    2) когда решите вопрос (1) с правильными названиями функций,
    только после этого можно переходить к правильным названиям переменных


    1. 1QDenisQ Автор
      02.02.2026 13:08

      Данные название были выбраны для пример :/. В данном случае можно бесконечно угодно спорить как правильно назвать. Ровным счётом я могу сказать что подходит названия get_db_player() и так далее. Это были примеры, а не финальная реализация. В таких случаях имена выбираются для наглядности, а не для соблюдения всех соглашений. Критиковать учебный или иллюстративный код за имена функций — странно, честно говоря. Либо тогда, возможно, стоит перечитать, что такое «пример», и зачем он вообще нужен. Мои навыки писать код проверяются не по именам в демонстрационном фрагменте.


  1. patex
    02.02.2026 13:08

    Как мне кажется тут больше проблема в auto, с прямым указанием типа проблема просто исчезает

    Useruser = get_user(user_id)

    Useradmin = get_user(admin_id)

    if(user=admin){

    hack_pentagon()

    }


  1. economist75
    02.02.2026 13:08

    Идея автора статьи хороша, а проблема актуальна. Требовать в линтере или CI/CD чтобы было user1 = get_user() - вполне разумно. В python/javascript мы легко это порешали регулярками по строке, в скрипте, запускающем автотесты. Дилемма snake/Camel оказалась первопричиной разнобоя. Фронты и бэки, после жуткого срача и попойки с дракой (да, у нас настоящий стартап) - провели компитишн. И оказалось что snake_case тупо проще и скорее в наборе на ~20%. На нем и остановились.


  1. LaRN
    02.02.2026 13:08

    Что делать если нужно два раза дернуть одну функцию из одного блока кода но с разными параметрами например? Имена переменых результатов будут конфликтовать с компилятором?