Мне нравятся простые языки программирования, такие как Gleam, Go и C. Знаю, я не один такой. Есть что-то чудесное в работе с простым языком: каково его читать, использовать в команде, возвращаться к нему спустя долгое время и т.д.

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

  1. Возможности, которые всегда под рукой
  2. Быстрые циклы итераций
  3. Единообразие выполнения любых вещей
  4. Принципы работы с функциями
  5. Простые системы статических типов
Ниже подробно обсудим каждую из этих идей.

Всегда под рукой


В философии технологии различаются две очень полезные концепции: «наличность» (presence-at-hand) и «подручность» (readiness-at-hand). Некая концепция считается наличной, если она сейчас занимает ваши мысли, находится у вас в оперативной памяти. Подручной же концепция считается в том случае, если мы можем даже не догадываться о её наличии, пока не попытаемся ею воспользоваться. Например, в тысячный раз заходя к себе на кухню, мы не вполне точно представляем, что именно лежит во всех этих шкафчиках, пакетиках, ящичках с продуктами, на столе, какие в кухне есть украшения и что-либо ещё. Я сравниваю такой заход на кухню с тем, как будто ты спонтанно заглянул в холодильник, чтобы перекусить. С другой стороны, когда я сажусь за стол, именно стол и стулья переходят для меня в категорию наличных, хотя только что были подручными. У меня в этот момент холодильник отходит на задний план и становится подручным.

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

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

Я разобрал здесь эту идею подручности, поскольку в простых языках программирования зачастую действительно есть много таких возможностей, которые специально реализованы так, чтобы не «забивать эфир», когда мы ими не пользуемся. Например, для Gleam, Go и C характерна выраженная кроссплатформенность, и поддержка множества платформ — это большой кусок работы, которой приходится заниматься при программировании на них. Когда требуется обеспечить работоспособность вашего кода в браузере, или на Raspberry Pi, или на смартфоне, или на сервере, в язык для этого добавляются конкретные возможности, которые, однако, никак не вредят его простоте. Ещё один пример — поддержка протокола языкового сервера (LSP), которому уделяется большое внимание среди разработчиков под Gleam и Go, а на C этот протокол поддерживается очень прилично, несмотря на возраст языка.

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

Циклы итераций


Очень быстрый цикл итераций (обычно итерация приравнивается к времени компиляции) — как раз тот аспект, реализовать который стремятся разработчики большинства простых языков. Если итерация проходит быстро, то и затраты на прототипирование и эксперименты совсем невелики, а разработчик может не выпадать из состояния потока.

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

Но в Gleam и Go производительность компилятора — одна из лучших в своём классе. Go этим славится, так что здесь я не буду особенно распространяться. Компилятор Gleam написан на Rust, и его разработчики ясно дали понять, что автономным (self-hosted) ему не бывать, поскольку из-за этого снизится производительность и усложнится дистрибуция. Когда это только возможно, синтаксический разбор и обработка файлов распараллеливаются, и, как минимум, мои проекты на Gleam компилируются мгновенно.

Также стоит упомянуть действующую в Gleam систему зависимостей, она крайне симпатичная. Она работает с менеджером пакетов Hex, применяемым в Erlang и Elixir, поэтому здесь генерируются аккуратные страницы документации в формате HexDocs. Поэтому вам несложно находить библиотеки, а хорошая документация становится нормой. Чтобы убедиться, насколько удобнее всё делается в Gleam, рассмотрим, какие варианты предоставляются, когда я ввожу в командную строку gleam и жму enter:

 $ gleam

gleam 1.0.0

USAGE:
    gleam <SUBCOMMAND>

OPTIONS:
    -h, --help       Print help information
    -V, --version    Print version information

SUBCOMMANDS:
    add        Add new project dependencies
    build      Build the project
    check      Type check the project
    clean      Clean build artifacts
    deps       Work with dependency packages
    docs       Render HTML documentation
    export     Export something useful from the Gleam project
    fix        Rewrite deprecated Gleam code
    format     Format source code
    help       Print this message or the help of the given subcommand(s)
    hex        Work with the Hex package manager
    lsp        Run the language server, to be used by editors
    new        Create a new project
    publish    Publish the project to the Hex package manager
    remove     Remove project dependencies
    run        Run the project
    shell      Start an Erlang shell
    test       Run the project tests
    update     Update dependency packages to their latest versions

Много совершенно прямолинейных и удобных подкоманд! Я успел поработать с Gleam уже несколько месяцев, опубликовал пару пакетов, а много других добавил в мои проекты, и пока меня всё устраивает.

Единообразие выполнения любых вещей


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

Но зачастую в таких языках это аргументируется так: жертвуя чем-то ради производительности, мы делаем отдельные компоненты языка только лучше. В Go принято укладывать весь циклический код в цикл for, весь код из разряда «то или это» должен быть заключён в операторах if, а любой код на «выбор из множества» — в операторах switch. Именно поэтому циклы for и операторы switch в Go немного необычные, а цикла while нет вообще. История о конкурентности в Go подчиняется одному подходу, а в Rust — совершенно противоположному. В какой-то степени здесь можно писать код и в функциональном стиле, но писать на Go лямбда-выражения — это настоящий геморрой. В системе типов Go любые нетривиальные задачи решаются при помощи интерфейсов.

В Gleam эта идея развита ещё сильнее. У него функциональная родословная, поэтому там нет циклических конструкций, только рекурсия и такие вещи как map и fold. Применяется оптимизация хвостовых вызовов, поэтому подобный код компилируется во многом именно так, как если бы он был заключён в цикл while. Более того, в Gleam даже нет if! Напротив, там есть только (мощный) механизм сопоставления с шаблоном при наличии (мощных) ограничивающих условий. Вычисление ряда Фибоначчи можно было бы запрограммировать так:

pub fn fib(n: Int) -> Int {
  case n < 2 {
    True -> n
    False -> fib(n - 1) + fib(n - 2)
  }
}

Сопоставление с шаблоном в соответствии с True и False работает точно, как оператор if, поэтому на практике подобное «ограничение» никогда особо не раздражает.

Кроме того, в Gleam навязывается змеиный регистр (snake_case) при именовании переменных и функций, а типы именуются в стиле Pascal (PascalCase). Кроме того, в Gleam есть отличная система догматичного форматирования кода (точно, как в Go). При запуске проекта в Gleam среди прочего по умолчанию выполняется действие github по проверке форматирования. Да, так и есть! Ограничения такие, что вас быстро загоняют к специфическому стилю программирования, которым пользуются и все ваши коллеги.

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

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

Генеративный ИИ представляется мне эстетически-ориентированным в философском смысле, так как по природе своей он обрабатывает код «пословно» (а не как поток сознания), а также в силу его статистической основы. Таким образом, при работе с простыми языками вроде C, Go и Gleam, программы на которых всегда пишутся одинаково, подсказки ИИ будут отличаться высокой точностью. На этих языках во многом согласуется «эстетичность» кода с точки зрения человека и с точки зрения компьютера. Выше я привёл функцию для вычисления ряда Фибоначчи, и она была почти полностью сгенерирована Claude, без какого-либо редактирования, просто по ходу подготовки базы кода для этого поста (речь о небольшом или среднем приложении на Gleam). Я практически уверен, что в обучающем множестве Claude не было или почти не было кода на Gleam, а также этот язык было бы легко перепутать с Rust, поскольку синтаксис этих языков (намеренно) получился схожим. Но ИИ всё равно справился очень достойно.

Принципы работы с функциями


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

C и Go в этом отношении явно выделяются на фоне других языков. Оба этих языка поддерживают функции высшего порядка (Хотя, в C замыкания организованы во многом по принципу «сделай сам», и это неудивительно), но такой стиль кода совсем не идиоматический. Как я сказал выше, циклы должны снабжаться готовыми цикловыми конструкциями, а динамическое поведение, как правило, следует достигать иными способами. Это практически очевидно, когда пишешь код на Go и C, и в Go это определённо делается в большей степени по идеологическим, а не по технологическим причинам. Примерно так же устроены и лямбда-выражения в Python, но там такие черты менее выражены.

Можно подумать, что Gleam плохо укладывается в эту категорию, поскольку это чисто функциональный язык, но в его структуре предусмотрены механизмы и для работы в таком стиле. Привязки локальных переменных в Gleam не рекурсивны, это явно сделано для того, чтобы простимулировать программиста поднимать функции на верхний уровень. В Gleam применяется оператор |>, благодаря которому код высшего порядка гораздо проще читать и судить о нём. (Классный!) синтаксис use, применяемый в Gleam, охватывает большинство вариантов практического применения лямбда-выражений в функциях, поэтому складывается ощущение, будто пишешь удобный простой императвный код. Например, можно запрограммировать что-то подобное, напоминающее циклы for:

import gleam/int
import gleam/list
import gleam/io

/// для каждого i в списке выводим на экран i+1
pub fn print_all_plus_one(l: List(Int)) {
  // этот пример надуманный;  как правило, приходится использовать всего один цикл
  //один цикл:
  let res = {
    use i <- list.map(l)
    int.to_string(i + 1)
  }
  // другой цикл:
  use s <- list.each(res)
  io.print(s)
}

Обратите внимание, что код в таком стиле немного неаккуратный. В таком случае обычно не приходится пользоваться use, более того, можно обойтись вызовами list.each, в нормальном порядке. Я просто хотел показать, как use превращает функции высшего порядка в своего рода императивный код. Действительно, код такого типа время от времени попадался мне в базах кода на Gleam.

Если эти аспекты проектирования языков вам интересны, то, думаю, вам понравится и этот крутой пост.

Системы типов


Может возникнуть вопрос, а почему в моём списке нет Python. Причина, по которой писать код на Python приходится настолько иначе — это рефакторинг. Мне очень помогают системы типов из Gleam, Go и C, когда приходится вносить серьёзные изменения в мой код; таким образом, я могу не держать в голове много лишней информации. В Python я словно блуждаю в потомках, мне остаётся только догадываться, на какую следующую ошибку системы типов я наткнусь во время исполнения. В Python почти ничего не делается для того, чтобы облегчить мне управление проектом, поэтому до проекта просто страшно дотрагиваться сколь-либо значительным образом. Оптимизация удобочитаемости обычно идёт рука об руку с оптимизацией под рефакторинг.

С другой стороны, могут найтись читатели, которые добавили бы в мой список Haskell. Во-первых, в нём нет практически ничего кроме лямбд, верно? Да уж, простой язык. Честно говоря, я не думаю, что кому-то может прийти в голову добавить в этот список Haskell. В силу того, как там устроена система типов (и порочной культуры «брать случайные последовательности символов и с их помощью выражать сложные идеи — всё ради того, чтобы любая функция получалась однострочной»), Haskell никак не прост. Есть множество способов писать на нём код, и читать такой код невероятно сложно (хотя и очень весело, как только набьёшь в этом руку).

Простые языки удивительным образом балансируют на грани между выразительностью и скованностью. В C, Go и Gleam в той или иной форме предлагается динамическая типизация, явно рассчитанная на очень ограниченную область применения. Кроме того, во всех них есть некоторая причудливость для выражения нужных вам вещей без применения динамической типизации. В Go это делается при помощи интерфейсов, в Gleam — при помощи мощного полиморфизма, а в C — при помощи макросов препроцессора и приведений. В конце концов, системы типов очень лаконичны и ограничительны. Баланс, достигнутый в простых языках программирования, на практике очень приятен. Всё равно, как в мудрой семье удаётся найти золотую середину между свободой для ребёнка и разумным контролем. Ребёнку ничего не угрожает, и при этом он счастлив.

Заключение


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

Как вы уже догадываетесь, я весьма интересуюсь простыми языками программирования и планирую сам написать небольшой язык, в котором вышеописанные идеи были бы развиты ещё сильнее, чем в Gleam. В этом языке не должно быть лямбд, как и в OBJ. Кроме того, у меня есть некоторые идеи, как организовать работу без сборщика мусора, идеи для этого я позаимствую из языка Mojo, где есть интересная система проверки заимствований, удобная для взаимодействия с Python.

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

P.S. Обращаем ваше внимание на то, что у нас на сайте проходит распродажа.

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


  1. SpiderEkb
    27.03.2024 15:29
    +19

    Не совсем понятны критерии "простоты" языка. Вот классический Паскаль - простой?

    А Фортран (ранних версий, 4-й, или 77-й)?

    Ну и примеры... Человеку, незнакомому с языком далеко не сразу очевидно что делает вот это:

    /// для каждого i в списке выводим на экран i+1
    pub fn print_all_plus_one(l: List(Int)) {
      // этот пример надуманный;  как правило, приходится использовать всего один цикл
      //один цикл:
      let res = {
        use i <- list.map(l)
        int.to_string(i + 1)
      }
      // другой цикл:
      use s <- list.each(res)
      io.print(s)
    }

    Что такое use? Зачем оно? Как устроен "другой цикл"? Где он начинается и где заканчивается?

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


    1. vadimr
      27.03.2024 15:29
      +12

      Человек ещё заодно считает, что эти 8 строк проще, чем функция высшего порядка (map +1 list).


      1. SpiderEkb
        27.03.2024 15:29
        +1

        Вдогонку. Про простоту языка.

        Простой язык - это то, по коду которого сразу понятно как он будет работать? Или это язык где все сложные конструкции скрываются в одной строке кода?

        Вот попробуйте на С (к примеру) без внешних библиотек расписать что делает этот самый цикл

          let res = {
            use i <- list.map(l)
            int.to_string(i + 1)
          }


  1. CrazyOpossum
    27.03.2024 15:29
    +2

    Крайне поверхностная статья. Многие пункты - заслуга не языка, а экосистемы. Рассуждения про типы просто нелепые. В Си слабая типизация, Питон с аннотациями мало чем отличается. Хаскелл, конечно, же сложный язык, но вовсе не из-за "случайных последовательностей символов", и ещё там не только лямбды.


  1. Demon416
    27.03.2024 15:29
    +1

    Статью нейросеть писала?

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

    В любом случае сложность библиотек куда важнее сложности языка


    1. SpiderEkb
      27.03.2024 15:29
      +1

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

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


  1. same_one
    27.03.2024 15:29
    +1

    Уже долгое время самым простым языком среди не-эзотерических является Оберон ревизии 2013. Дедушка Вирт на склоне лет знал толк в простоте.


  1. rukhi7
    27.03.2024 15:29
    +1

    По мне так самый простой язык программирования C#, причем он же и самый сложный. Вы всегда можете выбрать, в каких конструкциях вам выражаться, в самых простых или посложнее.


    1. SpiderEkb
      27.03.2024 15:29

      А какими еще языками приходилось пользоваться? С чем сраниваете?

      Про шарп ничего не могу сказать - не приходилось на нем писать. Просто интересно.


      1. rukhi7
        27.03.2024 15:29
        +1

        C, C++, Perl, Basic, Pl/1, Fortran, assembler-s-ы, Java, Rubi


        1. tonx92
          27.03.2024 15:29

          Из тех что вы описали Шарп действительно один из самых приятных. Но думаю что подавляющее большинство интерпретируемых языков будет проще шарпа. В них просто меньше базовых конструкций. Руби в их семействе скорее белая ворона в плане подходов.


  1. dprotopopov
    27.03.2024 15:29

    Статья хороша. Жалко что это просто репост чужого (перевод).


  1. mr-garrick
    27.03.2024 15:29

    Самый простой язык это BASIC. Чтобы никаких указателей, лябд и прочей ереси, только человекочитаемые слова. Ну, может быть Pascal ещё. А C, Go и вот этот ещё какой-то, не знаю про него ничего, точно не простые.


    1. SpiderEkb
      27.03.2024 15:29

      Ну тут надо четко обозначить что понимать под "простотой".

      С точки зрения концепций, заложенных в язык - да. Бейсик, классический Паскаль достаточно просты. С уже сложнее т.к. чтобы на нем нормально писать надо четко себе представлять что такое переменная, что такое указатель, как они соотносятся, как все это в памяти будет лежать и т.п.

      Ко мне вот сейчас иногда заходят с вопросами по С (С/С++ у нас используются мало, это не "основные" языки, но иногда они удобнее для решения некоторых задач).

      Так вот чтобы человек, никогда с указателями не работавший, быстро в тему въехал, объясняю так:

      • Открываешь шкаф, ищешь коробку на которой написано "сахар" - это обращение к переменной по ее имени - var

      • Открываешь шкаф, там на третьей полке вторая коробка справа - это обращение по указателю - *var

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

      Если же говорить с точки зрения синтаксиса, то что бейсик, что паскаль (классический), что С достаточно просты - смотришь код и видишь алгоритм. И знаешь что вот эта строка кода не скрывает под собой никаких тайн. В отличии от:

        let res = {
          use i <- list.map(l)
          int.to_string(i + 1)
        }

      тут возникает куча вопросов - какой тип у res? Сколько он в памяти занимает? Что скрывается за use i <- list.map(l)? Сколько времени оно будет работать? Будут ли с памятью динамически манипулировть? Там внутри цикл или что? А что такое i? Что в нем будет храниться?

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


      1. rukhi7
        27.03.2024 15:29

        то что бейсик, что паскаль (классический), что С достаточно просты - смотришь код и видишь алгоритм.

        Это если там есть алгоритм. К сожалению часто встречается код, в котором автору алгоритм не получилось сформулировать, просто код в один прекрасный момент заработал, но никто не знает почему. Как раз с таким кодом в основном приходится разбираться, так как он хоть и заработал в основном (так сказать), но иногда все таки не работает, а требуется чтобы работал всегда.

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


      1. vadimr
        27.03.2024 15:29

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


        1. SpiderEkb
          27.03.2024 15:29
          +1

          С Си-подобными указателями возникает некоторая путаница в предмете указания (указатель на массив не отличается от указателя на его первый элемент).

          В целом - да. Это тоже часть идеологии С - есть адрес блока памяти, а помнить что там лежит уже на совести разработчика.

          В этом плане еще проще реализовано в RPG (с которым работаю последние 6+ лет).

          Там указатель - это просто адрес в памяти. Он не типизирован. Точнее, есть два типа - указатель на данные - pointer, получается вызовом %addr и указатель на процедуру - pointer(*proc) - получается вызовом %paddr().

          Адресной арифметики, как в С, нет. Т.е. можно делать инкремент/декремент адреса, но всегда на заданное количество байт.

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

          dcl-s var1 char(100);             // строка 100 символов
          dcl-s var2 char(100) based(ptr);  // строка 100 символов по указателю ptr
          
          // прежде чем использовать var2 нужно инициализировать ptr
          ptr = %addr(var1);
          
          // теперь var2 и var1 суть одно и то же
          // или можно аллоцировать память динамически
          ptr = %alloc(100);
          
          // но потом не забыть деаллоцировать
          dealloc ptr;

          Никто не запрещает связывать с одним указателем несколько переменных разных типов. Вся ответственность за последствия на совести разработчика.

          А в остальном - очень простой процедурный паскалеподобный (с легким привкусом PL/I в синтаксисе) язык для эффективного решения определенного класса задач (работа с БД и коммерческие вычисления). И очень самодостаточный - в языке для решения этих задач есть все что нужно. Никаких дополнительных библиотек в 90% случаев вам не потребуется. Все типы данных что есть в БД (decimal/numeric, char/varchar, date/timw/timestamp...), работа с датам, временем, строками, арифметика с фиксированной точкой (в т.ч. и операции с округлением) - все в языке. Работа с таблицами индексами - хоть напрямую, хоть SQL непосредственно в код встраивай.

          В последнее время расширили работу с массивами - цикл перебора for-each, проверка вхождения в массив in, разбиение строки на составляющие по разделителю %split, склейка в строку %concat и %concatarr. Есть динамические массивы. Есть поиск в массиве %lookup (и модификации типа %lookupge, %lookuple и т.п.), причем, если массив объявлен как сортированный, с модификаторами ascend/descend, то используется двоичный поиск, иначе - перебором по массиву.

          В последних версиях добавили перезагрузку процедур

          // Отправка сообщения в queLIFO/queFIFO очередь
          // Возвращает количество отправленных байт
          // в случае ошибки -1
          dcl-pr USRQ_SendMsg int(10) overload(USRQ_Send: USRQ_SendKey);
          
          dcl-pr USRQ_Send int(10) extproc(*CWIDEN : 'USRQ_Send') ;
            hQueue    int(10)                    value;                                  // handle объекта (возвращается USRQ_Connect)
            pBuffer   char(64000)                options(*varsize);                      // Буфер для отправки
            nBuffLen  int(10)                    value;                                  // Количество байт для отправки
            Error     char(37)                   options(*omit);                         // Ошибка
          end-pr;
          
          // Отправка сообщения в queKeyd очередь
          // Возвращает количество отправленных байт
          // в случае ошибки -1
          
          dcl-pr USRQ_SendKey int(10) extproc(*CWIDEN : 'USRQ_SendKey') ;
            hQueue    int(10)                    value;                                  // handle объекта (возвращается USRQ_Connect)
            pBuffer   char(64000)                options(*varsize);                      // Буфер для отправки
            nBuffLen  int(10)                    value;                                  // Количество байт для отправки
            pKey      char(256)                  const;                                  // Значение ключа сообщения
            nKeyLen   int(10)                    value;                                  // Фактический размер ключа
            Error     char(37)                   options(*omit);                         // Ошибка
          end-pr;

          Не вдаваясь в подробности - USRQ_Send и USRQ_SendKey - две реальных процедуры с одинаковым типом возвращаемого значения, но работающих по разному и с разным набором параметров. А USRQ_SendMsg - универсальная объединяющая обертка. Ее используем в коде, а компилятор уже подставит вызов нужной процедуры в зависимости от того с каким набором параметров оно вызывается.

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

          Пример:

          dcl-ds t_dsCPDL qualified template; // расшифровка срока хранения лога
            Units    char(1);                   // единицы (D/W/M/Y)
            Count    char(3);                   // количество
          end-ds;
          
          dcl-ds dsCPOCTL len(100) qualified;
            poSts  char(1);                 // Статус:
                                            //   • N – неактивна
                                            //   • Y – активна
                                            //   • O – вызов RRUCHK#HD
            poCPDL    char(4);              // Срок хранения истории в формате XNNN, где:
                                            //   • X – фромат срока (D – день, M – месяц, Y – год)
                                            //   • NNN – числовое значение указанного срока
            poRESA    char(10);             // Коды результатов на авторизацию
            poRESW    char(10);             // Коды результатов на ожидание
            poLogL    char(1);              // Режим логирования:
                                            //   • 0 - ошибки
                                            //   • 1 - +инфо
                                            //   • 2 - +отладка
            poMsgQ    char(10);             // Очередь для вывода сообщений
            /////////////////// переопредления ///////////////////
            dsCPDL    likeds(t_dsCPDL) samepos(poCPDL);
            ResA      char(1) dim(10)  samepos(poRESA);
            ResW      char(1) dim(10)  samepos(poRESW);
          end-ds;

          template тут - аналог typedef. Т.е. просто описание, без создания самой переменной. Переменная в данном случае описывается как like (для обычной переменной) или likeds для структуры

          Модификаторы samepos - это как раз перекрытия. Т.е. обращаясь к dsCPOCTL.poCPDL мы работаем со строкой 4 символа (вида D002, M001 и т.п.), а обращаясь к dsCPOCTL.dsCPDL - уже с ней же, но в виде структуры из элементов dsCPOCTL.dsCPDL.Units и dsCPOCTL.dsCPDL.Count. Аналогично - dsCPOCTL.poRESA - строка из 10-ти символов, а dsCPOCTL.ResA она же, но в виде массива из 10-ти отдельных символов.

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

          Это делает аналогию с коробкой неполной.

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

          А вот в незнакомом шкафу - да, будете по имени искать, просматривая все коробки в поисках той, на которой написано "сахар".


  1. nameisBegemot
    27.03.2024 15:29

    В мире есть два языка программирования - ассемблер и все остальные.

    Первый - сложный. Остальные учить можно хоть всем сразу, было бы время


    1. NotSure
      27.03.2024 15:29

      Я думаю, любой документированный набор инструкций проца проще плюсов.


    1. SpiderEkb
      27.03.2024 15:29

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