Развертывание древовидной рекурсии в итеративную программу

Я написал библиотеку, которая может генерировать произвольные строки с учетом объекта спецификации (https://github.com/rgrannell1/revexp), и я хочу преобразовать функцию, считывающую спецификацию, из рекурсивного алгоритма в итеративный алгоритм. Я сталкиваюсь с ошибками stackoverflow из-за глубины спецификаций, которые я просматриваю.

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

Вот пример объекта спецификации.

const data = {
  every: [
    {
      digit: { zero: false }
    },
    {
      repeat: {
        value: { digit: {} }
      }
    }
  ]
}

и минимальный пример того, как алгоритм в настоящее время пересекает спецификацию и генерирует строку, соответствующую спецификации.

const fromSpec = (data) => {
  if (data.every) {
    return fromSpec.every(data)
  } else if (data.digit) {
    return fromSpec.digit()
  } else if (data.repeat) {
    return fromSpec.repeat(data.repeat)
  }
}

fromSpec.digit = () => {
  return Math.floor(Math.random() * 10)
}

fromSpec.every = part => {
  let message = ''

  for (const elem of part.every) {
    message += fromSpec(elem)
  }

  return message
}



fromSpec.repeat = part => {
  let message = ''
  
  // -- just using fixed repeat for the example
  for (let ith = 0; ith < 10; ++ith) {
    message += fromSpec(part.value)
  }

  return message
}

const result = fromSpec(data)
result // 1034856872

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


person Róisín Grannell    schedule 03.08.2020    source источник


Ответы (2)


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

const fromSpec = (data) => {
    const stack = [data];
    let message = '';
    while (stack.length > 0) {
        const item = stack.pop();
        // Assumption based on the code in the question:
        //   'every', 'digit', and 'repeat' keys are mutually exclusive.
        if (item.every) {
            // Add items in reverse order, so that items are popped off the stack
            // in the original order.
            for (let i = item.every.length - 1; i >= 0; --i) {
                stack.push(item.every[i]);
            }
        } else if (item.digit) {
            message += String(Math.floor(Math.random() * 10));
        } else if (item.repeat) {
            for (let i = 0; i < 10; ++i) {
                stack.push(item.repeat.value);
            }
        }
    }
    return message;
}

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

Следующие ссылки могут быть актуальными.

person dannyadam    schedule 03.08.2020

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

Хорошие программисты знают, что поддержание сложности имеет большое значение для поддержания нашего рассудка. Я хочу сохранить свою рекурсивную программу, и я хочу, чтобы компьютер обрабатывал этот процесс за меня. Можно мне пирог и тоже его съесть?

Вот еще один способ решить проблему -

// example1.js

import { concat, upper, digit, str, toString } from './RevExp.js'

const licensePlate =
  concat(upper(), upper(), upper(), str("-"), digit(), digit(), digit())

console.log(toString(licensePlate))
console.log(toString(licensePlate))
console.log(toString(licensePlate))
// RFX-559
// VKT-794
// KSF-823

Приступим к написанию модуля RevExp. Мы начнем с создания конструкторов для каждого из наших типов выражений -

// RevExp.js

const str = (value = "") =>
  ({ type: str, value })

const lower = () =>
  ({ type: lower })

const upper = () =>
  ({ type: upper })

const digit = ({ zero = true } = {}) =>
  ({ type: digit, zero })

const concat = (...exprs) =>
  ({ type: concat, exprs })

Теперь поработаем над RevExp.toString -

// RevExp.js (continued)

import { inRange } from './Rand.js'

const toString = (e) =>
{ switch (e.type)
  { case str:
      return String(e.value)
    case lower:
      return String.fromCharCode(inRange(97, 122))
    case upper:
      return String.fromCharCode(inRange(65, 90))
    case digit:
      return e.zero
      ? String(inRange(0, 9))
      : String(inRange(1, 9))
    case concat:
      return e.exprs.reduce((r, v) => r + toString(v), "")
    default: throw Error(`unsupported expression type: ${e.type}`)
  }
}

export { lower, upper, digit, alpha, repeat, concat, str, toString }

Должна быть возможность составлять сложные выражения, комбинируя несколько простых выражений. И мы представляем себе некоторые новые типы, такие как alpha и repeat -

// example2.js

import { alpha, digit, repeat, concat, str, toString } from './RevExp.js'

const segment =
  concat(alpha(), digit(), alpha(), digit(), alpha())

const serial =
  concat
    ( repeat
        ( concat(segment, str("-"))
        , { count: 4 }
        )
    , segment
    )

console.log(toString(serial))
console.log(toString(serial))
console.log(toString(serial))
// F3Q7U-b6k8Q-R8e3A-a2q3M-j0a9k
// g6G3w-h2O3O-b8O3k-L4p1y-m5I0y
// m6E0M-A4C2y-K3g0M-d7X7j-w8v5G

И добавить соответствующую поддержку в модуле RevExp -

// RevExp.js (enhanced)

import { inRange, sample } from './Rand.js'

const str = // ...
const lower = // ...
const upper = // ...
const digit = // ...
const concat = // ...

const alpha = () =>
  oneOf(upper(), lower())

const oneOf = (...exprs) =>
  ({ type: oneOf, exprs })

const repeat = (expr = {}, { count = 10 } = {}) =>
  ({ type: repeat, expr, count })

const toString = (e) =>
{ switch (e.type)
  { case str: // ...
    case lower: // ...
    case upper: // ...
    case digit: // ...
    case concat: // ...
    case oneOf:
      return toString(sample(e.exprs))
    case repeat:
      return toString(concat(...Array(e.count).fill(e.expr)))
    default: // ...
  }
}

export { /* ..., */ alpha, oneOf, repeat }

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

// RevExp.js (stack-safe)

// ...
import * as Str from './Str.js'
import { loop, recur, call } from './TailRec.js'

// ...
const toString = (e = {}) =>
  loop(toStringTailRec, e)

const toStringTailRec = e =>
{ switch (e.type)
  { case str: // ...
    case lower: // ...
    case upper: // ...
    case digit: // ...
    case concat:
      return e.exprs.length
        ? call
            ( Str.concat
            , recur(e.exprs[0])
            , recur(concat(...e.exprs.slice(1)))
            )
        : Str.empty
    case oneOf:
      return recur(sample(e.exprs))
    case repeat:
      return recur(concat(...Array(e.count).fill(e.expr)))
    default: throw Error(`unsupported expression type: ${e.type}`)
  }
}

export { /*...*/, toString } // <-- don't export toStringTailRec helper

А вот оставшиеся модули Str, Rand и TailRec -

// Str.js

const empty =
  ""

const concat = (a = "", b = "") =>
  a + b

export { empty, concat }
// Rand.js

const rand = (n = 2) =>
  Math.floor(Math.random() * n)

const inRange = (min = 0, max = 1) =>
  rand(max - min + 1) + min

const sample = (t = []) =>
  t[rand(t.length)]

export { rand, inRange, sample }

Написание модулей - важный фактор в создании повторно используемого кода. Этот TailRec модуль был написан в другом сообщении и может быть повторно использован без изменений 1 , чтобы удовлетворить потребности нашей программы.

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

// TailRec.js

const identity = x =>
  x

const call = (f, ...values) =>
  ({ type: call, f, values })

const recur = (...values) =>
  ({ type: recur, values })

const loop = (f, ...init) =>
{ const aux1 = (e, k) =>
    e.type === recur
      ? call(aux, e.values, r => call(aux1, f(...r), k))
  : e.type === call
      ? call(aux, e.values, r => call(aux1, e.f(...r), k))
  : call(k, e)

  const aux = (exprs, k) =>
    call
      ( exprs.reduce
          ( (mr, e) =>
              k => call(mr, r => call(aux1, e, x => call(k, [ ...r, x ])))
          , k => call(k, [])
          )
      , k
      )

  return run(aux1(f(...init), identity))
}

const run = r =>
{ while (r && r.type === call)
    r = r.f(...r.values)
  return r
}

export { loop, call, recur }

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

// example2.js

// ...
console.log(serial)
{ type: concat
, exprs:
    [ { type: repeat
      , expr:
          { type: concat
          , exprs:
              [ { type: concat
                , exprs:
                    [ { type: oneOf, exprs: [ { type: lower }, { type: upper } ] }
                    , { type: digit, zero: true }
                    , { type: oneOf, exprs: [ { type: lower }, { type: upper } ] }
                    , { type: digit, zero: true }
                    , { type: oneOf, exprs: [ { type: lower }, { type: upper } ] }
                    ]
                }
              , { type: str, value: "-" }
              ]
          }
      , count: 4
      }
    , { type: concat
      , exprs:
        [ { type: concat
          , exprs:
              [ { type: oneOf, exprs: [ { type: lower }, { type: upper } ] }
              , { type: digit, zero: true }
              , { type: oneOf, exprs: [ { type: lower }, { type: upper } ] }
              , { type: digit, zero: true }
              , { type: oneOf, exprs: [ { type: lower }, { type: upper } ] }
              ]
          }
        ]
      }
    ]
}

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


1. Незначительные изменения форматирования и переименование переменных для согласованности с этим ответом.

person Mulan    schedule 06.08.2020
comment
Это интересный подход; Сначала я начал с построителя стиля шаблона построителя, не слишком отличающегося от этого подхода (возможно, немного меньше FP), но решил перейти к более декларативному рекурсивному JSON (github.com/rgrannell1/revexp/blob/master/src/specs/), чтобы узнать о преимуществах проверки SEXPR, метапрограммирование, а межъязыковое ВГД проще. Подход, который вы включили, отлично подходит для использования, и его легче создавать новые изменения поверх - person Róisín Grannell; 06.08.2020
comment
Спасибо за чтение и комментирование. Подход здесь можно рассматривать как один уровень абстракции помимо рукописных литералов объектов. Выражения FP создают для нас рекурсивные объекты JS, и toString работает так же, как ваш fromSpec. Я сделал обновление, чтобы показать, как это выглядит, когда вы console.log выражение. - person Mulan; 06.08.2020
comment
Написав это вчера, я подумал, что было бы интересно поддержать RevExp.toString(RevExp.fromRegExp(/[A-Z]{3}-[0-9]{3}/)) для вывода (например) RFX-559. Это означало бы написать базовый лексер / парсер RegExp для набора функций, который вы хотите поддерживать. Это может быть даже лучше, чем любой из представленных здесь подходов ???? - person Mulan; 06.08.2020
comment
fromRegExp - это то, что я планировал добавить! Сначала я работал над компонентом на основе JSON, потому что я не хотел выражать весь AST JSON с помощью регулярных выражений ???? - person Róisín Grannell; 06.08.2020