О реализации ввода-вывода с именами

    Введение

    Как-то на одном из компьютерных форумов участники делились соображениями, чего им не хватает в языках, так сказать, в повседневной деятельности. Один заявил, что ему очень не хватает оператора типа put data. Для тех, кто не слышал о таком, поясню, что в языке PL/1 был предусмотрен оператор вывода (или ввода) значений переменных вместе с их именами, т.е. вместе с идентификаторами.

    Например, если обычный вывод put list(X,Y); давал что-нибудь вроде:

    1280    1024

    то оператор put data(X,Y); печатал:

    X=   1280  Y=   1024

    не заставляя писать для этого вывод в виде оператора:

    put list(’X=’,X,’Y=’,Y);

    Чаще всего это использовалось в отладочных целях, но не обязательно. Например, используя вывод в текстовый файл с помощью put data, а затем ввод этих же данных как исходных в другой программе с помощью «зеркального» оператора get data можно было получить удобную для чтения и редактирования форму представления исходных данных в текстовом виде.

    Можно возразить, что современные «оболочки» систем программирования легко позволяют вывести значения (и имена) любых объектов программы в интерактивном режиме безо всяких put data и поэтому в нем больше нет необходимости.

    Я тоже использую встроенный интерактивный отладчик, с помощью которого можно посмотреть любые, даже сложные (составные) данные. Однако потребность в возможностях put data не исчезла. Иногда нужен и анализ данных «на распечатке», (т.е. в логе) с возможностью быстрого и удобного добавления в текст программы вывода для отладки. К тому же для работы отладочных интерактивных систем используется дополнительная информация, которая обычно в выполняемом exe-файле отсутствует. В этом же случае все имена переменных «зашиты» в самой программе. Таким образом, удобства, предоставляемые языком в виде оператора, подобного put data, не заменяют, а дополняют возможности отладчиков. Кстати, в стародавние времена так называемая «посмертная» выдача на печать после ошибки иногда включала в себя и вывод полного списка всех имен переменных вместе с их текущими значениями. Некоторым программистам так нравился такой вывод, что они специально в конце своей программы ставили деление на ноль.

    Реализации ввода-вывода с именами

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

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

    2. Затем разбор повторяется заново, теперь полученные операции внутреннего представления программы будут использованы для генерации кода. Однако при разборе вместо лексического анализатора подключается его эмулятор, который уже не проводит лексический анализ исходного текста, а просто достает и выдает компилятору разобранные ранее и запомненные лексемы. Кроме них эмулятор может выдать и новые, сгенерированные им самим, лексемы.

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

    Для реализации put data достаточно проанализировать запомненные в буфере лексемы, найти конец разбираемого элемента (это запятая вне скобок или внешняя закрывающая скобка) и все предыдущие лексемы объединить в одну строку, которую в начале работы эмулятора и выдать с признаком «текстовая константа».

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

    Таким образом, например, текст:

    put data(x(i+1,j-1));

    внутри компилятора будет восприниматься как:

    put list(’x(i+1,j-1)=’,x(i+1,j-1));

    Разумеется, так будут обрабатываться не только объекты-переменные, но и любые выражения при выводе. Исключение составляет лишь вывод текстовых констант. Для них дополнительные лексемы, конечно же, не формируются во избежание бессмысленного вывода одного и того же текста два раза подряд со знаком равенства посередине.

    Ввод-вывод не скалярных объектов

    Идея исправлять исходный текст программы «на лету» позволила легко решить поставленную задачу. Однако полученный механизм жаль было использовать только для такой мелочи как put data, поскольку этот же подход можно было применить и для более объемной задачи – ввода-вывода не скалярных объектов. Автоматический ввод-вывод всех элементов не скалярного объекта также был предусмотрен в языке и тоже отсутствовал в исходной версии компилятора, хотя на практике часто требовался.

    Здесь ситуация сложнее, чем с put data для скаляра, так как для вывода массива вместо текста:

    put data(x);

    требовалось «на лету» сформировать уже что-нибудь вроде:

    do i=lbound(x) to hbound(x); put data(x(i)); end;

    Но и это еще не все. Например, если x – двумерный массив целых (например, матрица 3х3), тот же самый оператор put data(x); уже должен осуществлять вывод по двум циклам, чтобы дать в результате что-то такое:

    X(1,1)=    1 X(1,2)=    2 X(1,3)=    3 X(2,1)=    4 X(2,2)=    5 X(2,3)=    6 X(3,1)=    7 X(3,2)=    8 X(3,3)=    9

    Но при этом оператор put data(x(2)); должен вывести лишь одну строку матрицы:

    X(2,1)=    4 X(2,2)=    5 X(2,3)=    6

    А если x – это набор разнотипных элементов (в PL/1 такой набор называется структурой), то нужно еще выводить и имена отдельных полей этого набора, например:

    declare

    1 a,

     2 a1  fixed,

     2 a2  fixed,

     2 b,

      3 b1 fixed,

      3 b2 float;

    put data(a);

    A.A1=      0 A.A2=      0 A.B.B1=      0 A.B.B2=  0.000000E+00

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

    declare

    1 a(1:2),

     2 a1         fixed,

     2 a2         fixed,

     2 b,

      3 b1 (-5:3) fixed,

      3 b2 float;

    put data(b);

    B(1).B1(-5)=      0 B(1).B1(-4)=      0 B(1).B1(-3)=      0 B(1).B1(-2)=      0 B(1).B1(-1)=      0 B(1).B1(0)=      0 B(1).B1(1)=      0 B(1).B1(2)=      0 B(1).B1(3)=      0 B(1).B2=  0.000000E+00 B(2).B1(-5)=      0 B(2).B1(-4)=     0 B(2).B1(-3)=      0 B(2).B1(-2)=      0 B(2).B1(-1)=      0 B(2).B1(0)=      0 B(2).B1(1)=      0 B(2).B1(2)=   0 B(2).B1(3)=      0 B(2).B2=  0.000000E+00

    Однако в языке PL/1 есть хорошая основа для доработок в виде возможности записать оператор цикла прямо внутри оператора ввода-вывода. В свое время разработчикам языка вывод массивов казался очень важным. Поэтому они разрешили писать цикл ввода-вывода в виде:

    put list((x(i) do i=lbound(x) to hbound(x)));

    вместо обычного цикла:

    do i=lbound(x) to hbound(x); put list(x(i)); end;

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

    put list(1e0*(x1+x2)/2e0));

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

    Легко заметить, что вывод с именами (put data) и вывод не скалярных объектов – это дополняющие друг друга возможности. Объединяет их то, что в обоих случаях разбор объекта при трансляции выполняется два раза. Сначала все лексемы при лексическом разборе объекта запоминаются, а при повторном разборе выдаются из буфера так, как если бы из исходного текста. Но при этом в нужный момент выдаются и новые лексемы. В обоих случаях вместо лексического анализатора работает его эмулятор. Когда нужны обе возможности одновременно – работают оба эмулятора, один на более низком уровне выдает лексемы для индексов, названий полей структур и заголовков циклов, а второй добавляет к ним элементы с текстовыми константами-именами.

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

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

    Естественно, что если в исходном тексте старшие индексы объекта были явно указаны, то эмулятор дописывает только недостающие младшие индексы. Поэтому, например, для трехмерного массива x(1:10,1:10,1:10) оператор вывода:

    put list(x); выдаст 1000 значений

    put list(x(5)); выдаст 100 значений

    put list(x(5,6)); выдаст 10 значений.

    А для оператора put list(x(5,6,8)); эмулятор вообще не сгенерирует дополнительных лексем-индексов и будет напечатано единственное значение.

    Недостатком описанной реализации оказалось то, что при применении оператора put data к не скалярным объектам на печати вместо всех значений индексов печатались идентификаторы переменных циклов, которые подставлялись в исходный текст в момент трансляции. А поскольку для этой цели использовались служебные целые переменные ?A, ?B, ?С,…?O (всего 15 штук для максимально возможной размерности массива 15) выдача выглядела странно и во многом теряла ценность:

    X(?A,?B)=    1 X(?A,?B)=    2 X(?A,?B)=    3 X(?A,?B)=    4 X(?A,?B)=    5 X(?A,?B)=    6 X(?A,?B)=    7 X(?A,?B)=    8 X(?A,?B)=    9

    Для исправления этого недостатка потребовалось доработать системную библиотеку. В случае оператора put data с индексами компилятор не просто формирует текстовую константу – имя переменной с подставленными индексами ?A, ?B, ?С,…?O, но и дописывает перед каждым индексом специальный не выводимый символ 07FH.

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

    Переменные с именами при вводе

    Естественно, имеется парный оператор к put data - это get data, который должен вводить значения переменных, заданные вместе с их именами. Но здесь, на мой взгляд, разработчики PL/1 переусложнили данный оператор. В соответствии со стандартом языка оператор ввода должен не просто пропускать имена, но распознавать их и записывать следующее далее значение по нужному адресу. Например, если перетасовать значения, выданные put data(x);, в другом порядке:

    X(1,2)=    2 X(1,1)=    1 X(2,3)=    6 X(2,1)=    4 X(2,2)=    5 X(3,1)=    7 X(1,3)=    3 X(3,2)=    8 X(3,3)=    9

    То оператор get data(x); все равно должен ввести значения в правильном порядке, распознав каждое имя и индексы перед знаком равенства. И хотя такая обработка дает дополнительные удобства, например, можно пропускать данные, которые не меняются при вводе, все-таки это слишком хлопотно и сложно для реализации. А, главное, для такой возможности нужно иметь таблицу имен переменных и их адресов в каждом exe-файле, использующем get data. Поэтому я не стал реализовывать распознавание имен при вводе, а ограничился при чтении простым пропуском всех символов до знака равенства при разборе очередного элемента. Самое важное для меня – операторы put data и get data остались «зеркальными» и выдача данных оператором put позволяет потом безошибочно читать их оператором get с таким же списком объектов.

    Заключение

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

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

    Послесловие

    По личному опыту знаю, что, самая распространенная реакция на подобные заметки обычно сводится к двум заявлениям:

    1.  Это нафиг никому не нужно.

    2.  Это легко повторить на любом современном языке.

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

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

    Поэтому мне была предложена несложная библиотека на Немерле, реализующая, как уверял ее автор, вывод с именами и повторяющий возможности старого PL/1:

    using System.Console;
    using PL1.Macro;
    
    module Program
    {
      Main() : void
      {
        def x = 1280;
        def y = 1024;
        put data(x, y);
        put data(x, y, x + y, x.ToString() + "asd");
        _ = ReadLine();
      }
    }
    using Nemerle;
    using Nemerle.Compiler;
    using Nemerle.Compiler.Parsetree;
    
    using System.Collections.Generic;
    using System.Linq;
    
    namespace PL1.Macro
    {
      public macro PutData(args)
      syntax ("put", "data", args)
      {
        PutDataImpl.Impl(args)
      }
    
      module PutDataImpl
      {
        public Impl(args : PExpr) : PExpr
        {
          match (args)
          {
            | PExpr.Tuple as args =>
              def code = List();
              foreach (arg in args.args)
              {
                code.Add(<[ $(arg.ToString()) ]>);
                code.Add(<[ "=" ]>);
                code.Add(arg);
                code.Add(<[ " " ]>);
              }
              code.RemoveAt(code.Count - 1);
              def code = code.Aggregate((a1, a2) => <[ $a1 + $a2 ]>);
              <[ System.Console.WriteLine($(code)) ]>
            | _ => PExpr.Error(args.Location, "Tuple expected.")
          }
        }
      }
    }
    

    Однако на предложение доработать библиотеку, чтобы она позволяла выводить и не скалярные объекты с печатью всех индексов (и тогда действительно бы повторяла возможности PL/1), мне в ответ было предложено сделать это самому в виде «домашнего задания». Оказанную честь я отклонил, под предлогом того, что вообще-то у меня все это уже давно есть и используется. Судя по всему, автор библиотеки просто сразу не сообразил, как это сделать. И почему-то вспомнилась сцена из фильма «Формула любви», где Калиостро удивив всех поеданием вилки, все-таки отказался съесть при этом еще и тарелку.

    Комментарии 25

      +1
      1. Это нафиг никому не нужно.
      2. Это легко повторить на любом современном языке.


      Ну таки вообще говоря, да по обоим пунктам. :) Но аргументы будут.

      Почему не нужно? Потому что современные языки для ввода или вывода данных стараются использовать форматы стандартные, типа JSON скажем, если мы говорим именно об обмене данными — т.е. вы в одном месте вывели, а в другом прочитали. И вывели на одном языке, а прочитали на другом. Поэтому ввод/вывод в проприетарном формате put data, пусть и документированном — если не прям совсем никому не нужен, то скорее всего будет мало популярен. Тут очевидная проблема — насколько я помню, в PL/1 не было структуры данных, которую часто называют хешмап, а также вероятно других структур данных. Соответственно, ваш формат вывода просто не может предусмотреть все возможные виды данных, которые есть в других языках — то есть, он либо ограничен одним языком, либо будут те или иные проблемы (эти проблемы есть и в случае JSON тоже).

      Что же до легко повторить… ну конечно, не так чтобы прямо легко, сильно зависит от языка. В языках динамических, типа javascript — думается вообще раз плюнуть (потому что это прямой аналог того же JSON, вывод и ввод для которого уже имеется). Прямо в виде библиотеки. В языках с рефлексией, типа Java — тоже в общем не видно особых проблем (не считая наличия приватных полей, которые вообще говоря выводить не нужно, и что с ними делать — непонятно). Ну и во множестве языков есть специальные API для сериализации, которые нужны для того, чтобы передавать данные по разного рода каналам связи, сокетам и пр. — в этих случаях обычно есть нужный метод, который позволяет получить доступ к метаданным о переменных.
        +2

        На языке D это будет выглядеть так:


        void dump(vars...)()
        {
            import std.stdio;
        
            static foreach (i, v; vars)
            {
                write(vars[i].stringof, "=", v, "\t");
            }
        }
        
        void main()
        {
            auto x = 123;
            auto y = [1, 2];
            dump!(x, y); // x=123   y=[1, 2]    
        }

        https://run.dlang.io/is/xly7jl

          0
          Даже для 2D работает =)
           auto y = [[1, 2],[1,2]];
            0
            Для
            dump!(x, y, x + 45);

            Говорит

            Error: variable x cannot be read at compile time
              0

              Если хочется именно выражения выводить, то тут уже не так красиво:


              string dump(string exprs)()
              {
                  import std.algorithm, std.conv;
              
                  auto args = exprs.splitter(";").map!q{ "\"" ~ a ~ " = \"," ~ a }.joiner(",'\t',").to!string;
                  return "import std.stdio; writeln(" ~ args ~ ");";
              }
              
              void main()
              {
                  auto x = 123;
                  auto y = [[1, 2], [3, 4]];
                  mixin(dump!q{x+45;y}); // x+45 = 168    y = [[1, 2], [3, 4]]
              }

              https://run.dlang.io/is/B7eVH9

          0
          Для C# можно такое через expression tree сделать, что-то типа
          static void Main(string[] args)
          {
            int a = 10;
            int b = 20;
          
            var c = new int[] { 1, 2, 3};
                      
            Console.WriteLine(Data(() => d(a + b, c[1], "string literal", c)));
          }
          
          static string Data(Expression<Func<object>> expr)
          {
              // walk over expression, execute and convert values to string
              return "";
          }
          
          // fake method for the sake of building expression tree
          static object d(params object[] o)
          {
              return null;
          }

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

          () => d()

          Второй вариант это через пост-процессинг исходников или байт кода. Т.е. идём по коду и везде где встречаем вызов метода Data
          Data(a + b, c[1], "string literal", c)

          подменяем на вызов Data2
          Data2(new[] {"a + b", "c[1]", null, "c"}, a + b, c[1], "string literal", c)

          т.е. первый параметр это массив имён/выражений последующих параметров. В java такой вариант тоже легко сделать.

          Но как правильно заметил sshikov, надо делать как принято, а не как было сделано в другом языке. Тем более с нормальным дебагером, всё это превращается в маргинальные случаи.
            0
            Всё как вам хочется.

            мне хочется так:

            test:proc main;
            declare i fixed, x(9) float;

            do i=1 to hbound(x); x(i)=i; end i;
            put data(x);

            end test;

            X(1)= 1.000000E+00 X(2)= 2.000000E+00 X(3)= 3.000000E+00 X(4)= 4.000000E+00
            X(5)= 5.000000E+00 X(6)= 6.000000E+00 X(7)= 7.000000E+00 X(8)= 8.000000E+00
            X(9)= 9.000000E+00
            Конец программы
              0
              И так тоже нравится:

              test:proc main;
              declare (i,j) fixed, x(3,3) float;

              do i=1 to hbound(x,1);
              do j=1 to hbound(x,2);
              x(i,j)=i+j;
              end j;
              end i;
              put data(x);

              end test;

              X(1,1)= 2.000000E+00 X(1,2)= 3.000000E+00 X(1,3)= 4.000000E+00 X(2,1)=
              3.000000E+00 X(2,2)= 4.000000E+00 X(2,3)= 5.000000E+00 X(3,1)= 4.000000E+00
              X(3,2)= 5.000000E+00 X(3,3)= 6.000000E+00
              Конец программы
                –1

                Легко оба случая, рефлексия это сильная сторона c#.


                Ещё раз повторю, основной вопрос зачем.


                Для сериализации это не подойдёт.


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


                Остаётся только отладка. Но для отладки есть гораздо более удобные инструменты. Я могу добавить все меня интересующие переменные в окно мониторинга и смотреть на них в динамике по мере исполнения программы, а не на единовременный срез. Могу даже задать условие при котором программа приостановит выполнение (например x > 10).


                Так что да, остаётся кейс — отладка для олдфага, потому что он так привык и не хочет учится новому.

                  0
                  К чему все эти слова? Да приведите, наконец, эти 10-15 строчек кода, которые повторяют два примера и все.
                    0
                    Вывод массива в stdout
                    var x = new float[3,3];
                    
                    for(int i = 0; i < x.GetLength(0); i++)
                         for (int j = 0; j < x.GetLength(1); j++)
                              x[i, j] = i + j;
                    
                    x.ForEach<float>((value, coords) => Console.Write($"x[{String.Join(", ", coords)}]={value}"));
                    используя MultidimensionalArrayExtensions.

                    Результат

                    x[0, 0]=0 x[0, 1]=1 x[0, 2]=2 x[1, 0]=1 x[1, 1]=2 x[1, 2]=3 x[2, 0]=2 x[2, 1]=3 x[2, 2]=4

                    Городить (никому не нужное) универсальное решение ради того чтобы вам что-то доказать, смысла не вижу (за деньги могу).

                    Тот кто знает c#, по тому что уже приведено, видит что это возможно сделать.
                      0
                      Console.Write и PUT DATA — это, конечно, одного поля ягоды. Для отладки вполне годятся.
                      Но Вы всерьез считаете, что так должна выглядеть строка отладочной печати?

                      x.ForEach((value, coords) => Console.Write($«x[{String.Join(», ", coords)}]={value}"));
                      Это быстро вставить?
                      По-моему, отладочная печать должна выглядеть не сложнее вот такого:

                      put skip data(x(1)); // печать строки из 3 элементов
                      put skip data(x(1,1)); // печать одиночного элемента

                      X(1,1)= 2.000000E+00 X(1,2)= 3.000000E+00 X(1,3)= 4.000000E+00
                      X(1,1)= 2.000000E+00

                      В статье я не утверждал, что невозможно. Но пока в приведенных примерах получается коряво, неудобно и, да, не универсально.
                        –1
                        Вы похоже не хотите понять, что вам показывается принципиальная возможность этого добиться срествами языка.

                        Сделайте бибилиотеку и и скройте всё это в ней. Результат будет как я в первом примере показал
                        Data(() => d(a + b, c[1], "string literal", c))
                        Отличие от вашего примера минимально.

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

                        Я вам говорю, сделать такое можно. Главный вопрос зачем и объяснил что никому не надо. Notebook-и в python, это круто, поэтому и для c# сделали. Посмотрите, для интереса, куда двигаются с выводом отладочной информации, а вывод значений трехмерного массива с миллионом элементов на консоль — это так себе затея.

                        Давайте например вы покажите как у вас будет реализовано например такая задача.

                        Есть три массива и надо создать четвёртый в зависимости от того есть ли элементы второго и третьего в первом и отсортировать в порядке убывания.
                        int[] a = { 10, 20, 30, 40, 50, 60};
                        int[] b = { 10, 40, 50, 60 };
                        int[] c = { 30, 40, 60 };
                                    
                        Console.WriteLine(string.Join(", ", a.Where(w => b.Contains(w) && c.Contains(w)).OrderByDescending(o => o)));

                        результат

                        60, 40

                        В c# это получается одна строчка. Сколько вы будете пыхтеть в PL/1?
                          0
                          Извините, но Вы соскочили с темы, которая всего лишь об отладочном выводе с именами.
                          Кстати (см. соседний комментарий), в Питоне подумали и ввели что-то подобное. И не в библиотеку, а в язык.
                            –1
                            Не соскочил, а пытаюсь вам сказать вы хотите чтобы язык поддерживал нечто, что вам надо и в том виде как это вам надо. Результат достигается десятью другими способами, но вам хочется так как вы привыкли.

                            Вот например как отладочная информация должна выглядеть

                            nbviewer.jupyter.org/github/waltherg/notebooks/blob/master/2013-12-03-Crank_Nicolson.ipynb

                            (поищите на странице «print A_u» для примера вывода матрицы на консоль).

                            Это не веб страница, а как раз программа, вывод которой является то что вы видите. Вы можете её загрузить себе на компьютер, поменять код, исполнить и посмотреть на результат.
                    +1
                    Довольно глупо вместо ответа на вопрос, убеждать собеседника, что ему это не надо, а надо совсем другое. Особенно, если сам не можешь верно ответить =)

                    Например для отладки, если надо что то срочно поправить, а под рукой нет монструозной Студии.

                    Кстати, C# как то совсем уныло выглядит в плане форматированного вывода уже для 2D массива (
                      0
                      Довольно глупо вместо ответа на вопрос, убеждать собеседника, что ему это не надо, а надо совсем другое.
                      Нет, я вполне допускаю что это то что ему надо. Я как бы пытаюсь сказать что для отладки придумали поновее возможности.
                      Особенно, если сам не можешь верно ответить =)
                      В чём заключается верность ответа? Я показал принципиальную возможность сделать это даже так как ему это хочется. Сделать это так как это обычнно делается вообще не проблема.
                      Кстати, C# как то совсем уныло выглядит в плане форматированного вывода уже для 2D массива
                      c# даёт интструмент, вы можете сделать так как вам надо.

                      Например есть

                      put data(x);

                      X(1,2)= 2 X(1,1)= 1 X(2,3)= 6 X(2,1)= 4 X(2,2)= 5 X(3,1)= 7 X(1,3)= 3 X(3,2)= 8 X(3,3)= 9


                      Тут приходит Dukarov2 и говорит мне так не надо, а надо

                      X(1,2) : 2
                      X(1,1) : 1
                      X(2,3) : 6
                      ...
                      X(3,2) : 8
                      X(3,3) : 9


                      что мы делаем в PL/1? Помёт мечем?

                      Т.е. мы должны в язык добавить «единственно верное» решение для вывода данных. В c# сказали — мы вам дали ToString, надо — реализуйте своё виденье того как вам надо, будет как вы хотите.
              +3
              В последних версиях python-а есть в чем-то схожая с put data штатная возможность:
              x = 3
              l = [2, 4, 10]
              s = 'Example'
              print(f'{x=}, {x+2=}, {l=}, {s=}')
              
              напечатает:
              x=3, x+2=5, l=[2, 4, 10], s='Example'
              Хотя например для печати индексов для каждого элемента массива нужно свой метод __repr__ определить или еще другими доп. средствами воспользоваться
                –1
                Хорошо, вспомнили, что помогало отладке 50 лет назад и добавили ))
                Но вот с индексами массива не очень. Для больших массивов легко сбиться со счета.
                0
                В Julia удобно сделан вывод массивов
                julia> mat1 = [1 2 3; 4 5 6]
                2×3 Array{Int64,2}:
                 1  2  3
                 4  5  6

                А также для длинных массивов
                Заголовок спойлера
                julia> x = [j for j in 1:50]
                50-element Array{Int64,1}:
                  1
                  2
                  3
                  4
                  5
                  6
                  7
                  8
                  9
                 10
                 11
                 12
                 13
                  ⋮
                 39
                 40
                 41
                 42
                 43
                 44
                 45
                 46
                 47
                 48
                 49
                 50

                  0
                  template haskell:
                  main = do
                    let x   :: Int = 1
                        xs  :: [Int] = [1, 2, 3]
                        arr :: Array Int String = listArray (0, 2) ["a", "b", "c"]
                    $(traceData 'x)
                    $(traceData 'xs)
                    $(traceData 'arr)

                  Вывод:
                  x = 1
                  xs = [1,2,3]
                  arr = array (0,2) [(0,"a"),(1,"b"),(2,"c")]

                  Сразу скажу, что можно любой формат вывода сделать, здесь используется стандартный Show.

                  Если есть вариант реализовать подобное не каким-то магическим способом, а средствами языка — то почему нет? Но встраивать подобную конструкцию в язык точно не стоит.
                    0
                    А для массивов 2d и старше автоматически индексы добавятся?
                      +1
                      main = do
                        let arr :: Array (Int, Int) String =
                              listArray
                                ((0, 0), (2, 2))
                                [ "a0", "b0", "c0"
                                , "a1", "b1", "c1"
                                , "a2", "b2", "c2"
                                ]
                        $(traceData 'arr)

                      arr = array ((0,0),(2,2)) [((0,0),"a0"),((0,1),"b0"),((0,2),"c0"),((1,0),"a1"),((1,1),"b1"),((1,2),"c1"),((2,0),"a2"),((2,1),"b2"),((2,2),"c2")]
                        0
                        Чуток скобок многовато, но да — все отображено, для отладки то, что надо.
                        У меня самый громоздкий вариант структура-массив (т.е. есть и именованные поля и индексы):

                        declare
                        1 a(1:2),
                        2 a1 fixed,
                        2 a2 fixed,
                        2 b,
                        3 b1 (-5:3) fixed,
                        3 b2 float;

                        put data(b);

                        B(1).B1(-5)= 0 B(1).B1(-4)= 0 B(1).B1(-3)= 0 B(1).B1(-2)= 0 B(1).B1(-1)= 0 B(1).B1(0)= 0 B(1).B1(1)= 0 B(1).B1(2)= 0 B(1).B1(3)= 0 B(1).B2= 0.000000E+00 B(2).B1(-5)= 0 B(2).B1(-4)= 0 B(2).B1(-3)= 0 B(2).B1(-2)= 0 B(2).B1(-1)= 0 B(2).B1(0)= 0 B(2).B1(1)= 0 B(2).B1(2)= 0 B(2).B1(3)= 0 B(2).B2= 0.000000E+00

                        Получится подобное?
                          +2
                          И?

                          #!/usr/bin/python3
                          
                          x={"red" : "красный", "green": True}
                          print(x)
                          


                          {'red': 'красный', 'green': True}


                          Получится такое? ;)
                          Как там в древних языках с ассоциативными массивами, например?

                          Просто 50 лет назад со средствами отладки было сложно, и оператор put был полезен. Раз в языки, с помощью которых решаются современные задачи, этот оператор не имплементирован — значит для решения современных задач он не нужен. Для отладки придумали отладчики — это удобнее, чем простыня из put. Массивы они всякие бывают… Многомерный массив в миллион элементов дампить так себе идея, например.

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

                  Самое читаемое