Проблема с рекурсивным присоединением массива, сформированного из абстрактной синтаксической древовидной структуры

#javascript #recursion #abstract-syntax-tree

#javascript #рекурсия #abstract-syntax-tree

Вопрос:

У меня есть грубая структура, подобная AST; массив, содержащий либо строку, либо объект, где объект имеет идентификатор и аргументы, где аргументы представляют собой массив либо строки, либо объекта, как указано выше.

Цель состоит в том, чтобы вывести строку из соединенных строк и строк объектов, построенных рекурсивно (из-за природы структуры).

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

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

 const argLists = [
  [
    "one",
    { id: "$id1", args: [["two", "three"], "4", "3", "$1-"] },
    "four",
    "five",
    { id: "$id1", args: [["six", "seven"], "3", "4", "$1-"] }
  ],
  ["(", "$ ", "$1", "$ ", "text", "$ ", "$2", "$ ", ")", "$3-"],
  [
    {
      id: "$id1",
      args: [
        [
          "one",
          "$ ",
          { id: "$ ", args: ["two", { id: "$chr", args: ["44"] }, "three"] },
          "$ ",
          "four"
        ],
        "4",
        "5",
        "$1-"
      ]
    }
  ]
]

function joinArgs(args = [], output) {
      for (let i = 0; i < args.length; i  ) {
        let arg = args[i];
        if (typeof arg === "string") {
          i--;
          let tempOutput = "",
            prev = args[i] || "",
            join = "",
            next = args[i   2] || "";
          if (typeof prev === "string" amp;amp; typeof next === "string") {
            join  = arg === "$ " ? "" : `${arg}`;
            tempOutput = `${prev}${join}${next}`;
          } else if (typeof prev === "string" amp;amp; typeof next === "object") {
            tempOutput = `${prev}${join}${joinArgs(
              next.args,
              output
            )}`;
          } else if (typeof prev === "object" amp;amp; typeof next === "string") {
            tempOutput = `${joinArgs(
              prev.args,
              output
            )}${join}${next}`;
          }
          i  = 3;
          output  = tempOutput;
        } else if (typeof arg === "object") {
          if (Array.isArray(arg)) {
            output  = joinArgs(arg, output);
          } else {
            if (arg.id === "$ ") {
              output  = joinArgs(arg.args, output);
            } else if (arg.id === "$chr") {
              output  = String.fromCharCode(arg.args[0]);
            } else if (arg.id === "$id1") {
              const id1Args = [];
              for (
                let id1ArgIdx = 0;
                id1ArgIdx < arg.args.length;
                id1ArgIdx  
              ) {
                let id1Arg = arg.args[id1ArgIdx];

                if (Array.isArray(id1Arg)) {
                  id1Arg = joinArgs(id1Arg, output).trim();
                }
                id1Args.push(id1Arg);
              }
              output  =
                " "
                // This function is irrelevant to the problem; but it does return a string
                //id1(
                //  ...id1Args.slice(0, id1Args.length - 1),
                //  id1Args[id1Args.length - 1]
                //);
            }
          }
        }
      }
      return output;
}

argLists.forEach(arg => {
  console.log(joinArgs(arg, ""));
}); 

Данные для тестовых примеров:

 const argLists = [
  [
    "one",
    { id: "$id1", args: [["two", "three"], "4", "3", "$1-"] },
    "four",
    "five",
    { id: "$id1", args: [["six", "seven"], "3", "4", "$1-"] }
  ],
  ["(", "$ ", "$1", "$ ", "text", "$ ", "$2", "$ ", ")", "$3-"],
  [
    {
      id: "$id1",
      args: [
        [
          "one",
          "$ ",
          { id: "$ ", args: ["two", { id: "$chr", args: ["44"] }, "three"] },
          "$ ",
          "four"
        ],
        "4",
        "5",
        "$1-"
      ]
    }
  ]
]; 

Ожидаемый результат:

 const output = [
    "one (two three 4 3 $1-) four five (six seven 3 4 $1-)",
    "($1text$2) $3-",
    "(onetwo,threefour 4 5 $1-)"
]; 

Выходные данные должны быть объединены в соответствии с этими правилами:

  1. Если аргумент является строкой и является a $ , объедините предыдущее и следующее значения без пробела между ними( prevnext ) .
  2. Если аргумент является строкой и не является a $ , объедините предыдущее, текущее и следующее значения с пробелом между ними( prev current next ) .
  3. Если аргумент является объектом, получите его аргументы из args свойства и повторите по мере необходимости, однако:
    1. Если идентификатор объекта $ равен, объедините все аргументы без пробелов, повторяя по мере необходимости для каждого аргумента.
    2. Если идентификатор объекта $chr равен, возвращайтесь String.fromCharCode() с единственным аргументом, предоставленным объекту, заданному этому статическому методу.
    3. Если идентификатор объекта $id1 равен, обработайте первый аргумент как массив в соответствии с приведенными выше правилами (если это массив), а все остальные аргументы объедините пробелом и заключите все в круглые скобки.
    4. Технически возможно, что идентификатор объекта может не совпадать ни с одной из предыдущих трех возможностей, но такая вероятность не является проблемой, поскольку функция, вызываемая объектом, всегда будет возвращать строку.

Ответ №1:

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

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

 const joinObject = ({id, args}) => (({
  '$ ': (ns) => ns .map (joinArgs) .join (''),
  '$chr': ([n]) => String .fromCharCode (n),
  '$id1': ([ns, ...more]) => `(${joinArgs (ns)} ${more .map (joinArgs) .join (' ')})`,
}) [id] || ((args) => args .map (joinArgs) .join (' '))) (args)
//          `----------------------------------------' 
//                                `--- default.  Not sure what belongs here

const joinArray = (args) => args .reduce(
  ({str, skip}, arg) => ({
    str: str   (skip || arg == '$ ' ? '' : ' ')   (arg == '$ ' ? '' : joinArgs(arg)), 
    skip: arg == '$ '
  }),
  {str: '', skip: true}
).str

const joinArgs = (args) =>
  Array .isArray (args) 
    ? joinArray (args) 
  : Object (args) === args 
    ? joinObject (args) 
  : args


const argLists = [["one", {id: "$id1", args: [["two", "three"], "4", "3", "$1-"]}, "four", "five", {id: "$id1", args: [["six", "seven"], "3", "4", "$1-"]}], ["(", "$ ", "$1", "$ ", "text", "$ ", "$2", "$ ", ")", "$3-"], [{id: "$id1", args: [["one", "$ ", {id: "$ ", args: ["two", {id: "$chr", args: ["44"]}, "three"]}, "$ ", "four"], "4", "5", "$1-"]}]]

argLists.forEach(args => console.log(joinArgs(args))) 

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

Большой трюк здесь — взаимная рекурсия. Это может значительно упростить код, который вы пишете в подобных обстоятельствах. Другим полезным методом является объект, содержащий обработчики для запуска для разных id значений объекта. Я нахожу это очень полезным для отделения основных частей логики от того, что склеивает их вместе.

Ответ №2:

Это работает для всех предоставленных вами тестовых примеров.

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

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

 outputString  = "("   joinArgs(current_arg.args)   ") ";
 

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

 const argLists = [
  [
    "one",
    { id: "$id1", args: [["two", "three"], "4", "3", "$1-"] },
    "four",
    "five",
    { id: "$id1", args: [["six", "seven"], "3", "4", "$1-"] }
  ],
  ["(", "$ ", "$1", "$ ", "text", "$ ", "$2", "$ ", ")", "$3-"],
  [
    {
      id: "$id1",
      args: [
        [
          "one",
          "$ ",
          { id: "$ ", args: ["two", { id: "$chr", args: ["44"] }, "three"] },
          "$ ",
          "four"
        ],
        "4",
        "5",
        "$1-"
      ]
    }
  ]
];

function joinArgs(args = []) {
  outputString = "";
  for (let i = 0; i < args.length; i  ) {
    var current_arg = args[i];

    //check for string condition
    if (typeof current_arg === 'string') {
      outputString  = current_arg   " ";
    }

    //check for object condition
    if (typeof current_arg === "object") {

      if (current_arg.id == '$id1'){
        //if object has $id1 id, call joinArgs with the object's args as args parameter
        //surround object's returned output with parenthesis
        outputString  = "("   joinArgs(current_arg.args)   ") ";
      }

      //no recursive call needed here
      if (current_arg.id == '$chr') {
        outputString  = "$  "   String.fromCharCode(current_arg.args);
      }

      //call joinArgs with object args as args parameter
      if (current_arg.id == '$ ') {
        outputString  = joinArgs(current_arg.args);
      }

      // if object is array call joinArgs with the array as args paramater
      if (Array.isArray(current_arg)) {
        outputString  = joinArgs(current_arg);
      }
    }
  }

  //clean up outputString to remove unwanted " $  "
  outputString = outputString.replace(/ $  /g, "");
  //remove whitespace before closing parenthesis
  outputString = outputString.replace(/ )/g, ")");

  return outputString;
}




argLists.forEach(arg => {
  //trim leading and trailing whitespace
  astString = joinArgs(arg).trim();
  console.log(astString);
}); 

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

1. Вы, очевидно, знаете, как работает рекурсия. Мне, вероятно, не нужно было ничего объяснять