Как заменить циклы рекурсией в Javascript? - PullRequest
2 голосов
/ 26 апреля 2020

У меня есть 2 for петли, которые хорошо работают для создания сетки со строками и столбцами, но я хотел бы улучшить решение с помощью рекурсии, так как оно более чистое и рекомендуется (в функциональном программировании).

Желаемым выходным параметром является один массив пар, используемый в css grid

const createGrid = (rows,columns) => {
    let grid=[]
    for (let y = 3; y <= rows; y += 2){
        let row = []
        for (let x = 3; x <= columns; x += 2){
            let col = [y, x]
            row = [...row,col]
        }
        grid =[...grid, ...row]
    }
    return grid
}

Существуют ли также какие-либо рекомендации относительно того, как преобразовать циклы в рекурсивные решения, когда это возможно?

Ответы [ 3 ]

4 голосов
/ 26 апреля 2020

примитивная рекурсия

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

const makeGrid = (f, rows = 0, cols = 0) =>
  rows <= 0
    ? []
    : [ ...makeGrid(f, rows - 1, cols), makeRow(f, rows, cols) ]

const makeRow = (f, row = 0, cols = 0) =>
  cols <= 0
    ? []
    : [ ...makeRow(f, row, cols - 1), f(row, cols) ]

const g =
  makeGrid((x, y) => ({ xPos: x, yPos: y }), 2, 3)

console.log(JSON.stringify(g))
// [ [ {"xPos":1,"yPos":1}
//   , {"xPos":1,"yPos":2}
//   , {"xPos":1,"yPos":3}
//   ]
// , [ {"xPos":2,"yPos":1}
//   , {"xPos":2,"yPos":2}
//   , {"xPos":2,"yPos":3}
//   ]
// ]

Функциональный параметр f позволяет нам создавать ячейки сетки различными способами

const g =
  makeGrid((x, y) => [ x - 1, y - 1 ], 3, 2)

console.log(JSON.stringify(g))
// [ [ [ 0, 0 ]
//   , [ 0, 1 ]
//   ]
// , [ [ 1, 0 ]
//   , [ 1, 1 ]
//   ]
// , [ [ 2, 0 ]
//   , [ 2, 1 ]
//   ]
// ]

работать умнее, не сложнее

Согласно комментарию Берги, вы можете уменьшить некоторые дополнительные аргументы, используя curried конструктор ячейки -

const makeGrid = (f, rows = 0, cols = 0) =>
  rows <= 0
    ? []
    : [ ...makeGrid(f, rows - 1, cols), makeRow(f(rows), cols) ]

const makeRow = (f, cols = 0) =>
  cols <= 0
    ? []
    : [ ...makeRow(f, cols - 1), f(cols) ]

const g =
  makeGrid
    ( x => y => [ x, y ] // "curried" constructor
    , 2
    , 3
    )

console.log(JSON.stringify(g))
// [ [ [ 1, 1 ]
//   , [ 1, 2 ]
//   , [ 1, 3 ]
//   ]
// , [ [ 2, 1 ]
//   , [ 2, 2 ]
//   , [ 2, 3 ]
//   ]
// ]

есть свой торт и ешьте его тоже

В качестве альтернативы, мы можем включить предложение и по-прежнему принимать двоичную функцию при вызове сайт с частичным применением -

const makeGrid = (f, rows = 0, cols = 0) =>
  rows <= 0
    ? []
    : [ ...makeGrid(f, rows - 1, cols)
      , makeRow(_ => f(rows, _), cols) // <-- partially apply f
      ]

const makeRow = (f, cols = 0) =>
  cols <= 0
    ? []
    : [ ...makeRow(f, cols - 1), f(cols) ]

const g =
  makeGrid
    ( (x,y) => [ x, y ] // ordinary constructor
    , 2
    , 3
    )

console.log(JSON.stringify(g))
// [ [ [ 1, 1 ]
//   , [ 1, 2 ]
//   , [ 1, 3 ]
//   ]
// , [ [ 2, 1 ]
//   , [ 2, 2 ]
//   , [ 2, 3 ]
//   ]
// ]

N-е измерение

Выше мы ограничены двумерными сетками. Что, если бы мы хотели 3-мерные или даже больше?

const identity = x =>
  x

const range = (start = 0, end = 0) =>
  start >= end
    ? []
    : [ start, ...range(start + 1, end) ] // <-- recursion

const map = ([ x, ...more ], f = identity) =>
  x === undefined
    ? []
    : [ f(x), ...map(more, f) ] // <-- recursion

const makeGrid = (r = [], d = 0, ...more) =>
  d === 0
    ? r
    : map(range(0, d), x => makeGrid(r(x), ...more)) // <-- recursion

const g =
  makeGrid
    ( x => y => z => [ x, y, z ] // <-- constructor
    , 2       // <-- dimension 1
    , 2       // <-- dimension 2
    , 3       // <-- dimension 3
    , // ...     <-- dimension N
    )

console.log(JSON.stringify(g))

Выход

[ [ [ [0,0,0]
    , [0,0,1]
    , [0,0,2]
    ]
  , [ [0,1,0]
    , [0,1,1]
    , [0,1,2]
    ]
  ]
, [ [ [1,0,0]
    , [1,0,1]
    , [1,0,2]
    ]
  , [ [1,1,0]
    , [1,1,1]
    , [1,1,2]
    ]
  ]
]

любые размеры; плоский результат

Согласно вашему комментарию, вам нужен массив пар flat . Вы можете добиться этого, просто заменив map на flatMap, как показано ниже -

const identity = x =>
  x

const range = (start = 0, end = 0) =>
  start >= end
    ? []
    : [ start, ...range(start + 1, end) ]

const flatMap = ([ x, ...more ], f = identity) =>
  x === undefined
    ? []
    : [ ...f(x), ...flatMap(more, f) ] // <-- flat!

const makeGrid = (r = [], d = 0, ...more) =>
  d === 0
    ? r
    : flatMap(range(0, d), x => makeGrid(r(x), ...more))

const g =
  makeGrid
    ( x => y => [{ x, y }]  // <-- constructor
    , 2       // <-- dimension 1
    , 2       // <-- dimension 2
    , // ...     <-- dimension N
    )

console.log(JSON.stringify(g))
// [ { x: 0, y: 0 }
// , { x: 0, y: 1 }
// , { x: 1, y: 0 }
// , { x: 1, y: 1 }
// ]

Функциональный конструктор снова демонстрирует свою универсальность -

const g =
  makeGrid
    ( x => y =>
        [[ 3 + x * 2, 3 + y * 2 ]] // whatever you want
    , 3
    , 3
    )

console.log(JSON.stringify(g))
// [[3,3],[3,5],[3,7],[5,3],[5,5],[5,7],[7,3],[7,5],[7,7]]

Learn more

As другие показывают, что эта конкретная версия makeGrid с использованием flatMap эффективно вычисляет декартово произведение . К тому времени, как вы обернетесь вокруг flatMap, вы уже знаете монаду List!


больше торта, пожалуйста!

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

reset
  ( call
      ( (x, y) => [[ x, y ]]
      , amb([ 'J', 'Q', 'K', 'A' ])
      , amb([ '♡', '♢', '♤', '♧' ])
      )
  )

// [ [ J, ♡ ], [ J, ♢ ], [ J, ♤ ], [ J, ♧ ]
// , [ Q, ♡ ], [ Q, ♢ ], [ Q, ♤ ], [ Q, ♧ ]
// , [ K, ♡ ], [ K, ♢ ], [ K, ♤ ], [ K, ♧ ]
// , [ A, ♡ ], [ A, ♢ ], [ A, ♤ ], [ A, ♧ ]
// ]

Как и в случае с монадой List, выше amb инкапсулирует это понятие ambiguous ( недетерминированные c) вычисления. Мы можем легко написать наше 2-мерное simpleGrid, используя продолжения с разделителями -

const simpleGrid = (f, dim1 = 0, dim2 = 0) =>
  reset
    ( call
        ( f
        , amb(range(0, dim1))
        , amb(range(0, dim2))
        )
    )

simpleGrid((x, y) => [[x, y]], 3, 3)
// [[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2]]

Создание сетки N-размерности также очень просто благодаря amb. Реализация почти исчезла -

const always = x =>
  _ => x

const multiGrid = (f = always([]), ...dims) =>
  reset
    ( apply
        ( f
        , dims.map(_ => amb(range(0, _)))
        )
    )

multiGrid
  ( (x, y, z) => [[ x, y, z ]] // <-- not curried this time, btw
  , 3
  , 3
  , 3
  )

// [ [0,0,0], [0,0,1], [0,0,2]
// , [0,1,0], [0,1,1], [0,1,2]
// , [0,2,0], [0,2,1], [0,2,2]
// , [1,0,0], [1,0,1], [1,0,2]
// , [1,1,0], [1,1,1], [1,1,2]
// , [1,2,0], [1,2,1], [1,2,2]
// , [2,0,0], [2,0,1], [2,0,2]
// , [2,1,0], [2,1,1], [2,1,2]
// , [2,2,0], [2,2,1], [2,2,2]
// ]

Или мы можем создать желаемые приращения и смещения, используя line в конструкторе ячеек -

const line = (m = 1, b = 0) =>
  x => m * x + b // <-- linear equation, y = mx + b

multiGrid
  ( (...all) => [ all.map(line(2, 3)) ] // <-- slope: 2, y-offset: 3
  , 3
  , 3
  , 3
  )

// [ [3,3,3], [3,3,5], [3,3,7]
// , [3,5,3], [3,5,5], [3,5,7]
// , [3,7,3], [3,7,5], [3,7,7]
// , [5,3,3], [5,3,5], [5,3,7]
// , [5,5,3], [5,5,5], [5,5,7]
// , [5,7,3], [5,7,5], [5,7,7]
// , [7,3,3], [7,3,5], [7,3,7]
// , [7,5,3], [7,5,5], [7,5,7]
// , [7,7,3], [7,7,5], [7,7,7]
// ]

Так где же reset, call, apply и amb взялись? JavaScript не поддерживает продолжения первого класса, но ничто не мешает нам реализовать их самостоятельно -

const call = (f, ...values) =>
  ({ type: call, f, values })  //<-- ordinary object

const apply = (f, values) =>
  ({ type: call, f, values })  //<-- ordinary object

const shift = (f = identity) =>
  ({ type: shift, f })         //<-- ordinary object

const amb = (xs = []) =>
  shift(k => xs.flatMap(x => k(x))) //<-- returns ordinary object

const reset = (expr = {}) =>
  loop(() => expr)             //<-- ???

const loop = f =>
  // ...                       //<-- follow the link!

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

Первоклассные продолжения открывают мощный поток управления для ваших программ. Вы когда-нибудь задумывались, как JavaScript реализует function* и yield? Что если у JavaScript не было этих способностей? Прочтите пост , чтобы увидеть, как мы можем сделать это (и даже больше), используя только обычные функции.


демонстрация кода продолжения

Посмотрите, как это работает в вашем собственном браузере! Разверните фрагмент ниже, чтобы сгенерировать сетки, используя продолжения с разделителями ... в JavaScript! -

// identity : 'a -> 'a
const identity = x =>
  x

// always : 'a -> 'b -> 'a
const always = x =>
  _ => x

// log : (string, 'a) -> unit
const log = (label, x) =>
  console.log(label, JSON.stringify(x))
  
// line : (int, int) -> int -> int
const line = (m, b) =>
  x => m * x + b
  
// range : (int, int) -> int array
const range = (start = 0, end = 0) =>
  start >= end
    ? []
    : [ start, ...range(start + 1, end) ]

// call : (* -> 'a expr, *) -> 'a expr
const call = (f, ...values) =>
  ({ type: call, f, values })

// apply : (* -> 'a expr, * array) -> 'a expr
const apply = (f, values) =>
  ({ type: call, f, values })

// shift : ('a expr -> 'b expr) -> 'b expr
const shift = (f = identity) =>
  ({ type: shift, f })

// reset : 'a expr -> 'a
const reset = (expr = {}) =>
  loop(() => expr)

// amb : ('a array) -> ('a array) expr
const amb = (xs = []) =>
  shift(k => xs .flatMap (x => k (x)))

// loop : (unit -> 'a expr) -> 'a
const loop = f =>
{ // aux1 : ('a expr, 'a -> 'b) -> 'b
  const aux1 = (expr = {}, k = identity) =>
  { switch (expr.type)
    { case call:
        return call(aux, expr.f, expr.values, k)
      case shift:
          return call
            ( aux1
            , expr.f(x => trampoline(aux1(x, k)))
            , identity
            )
      default:
        return call(k, expr)
    }
  }

  // aux : (* -> 'a, (* expr) array, 'a -> 'b) -> 'b
  const aux = (f, exprs = [], k) =>
  { switch (exprs.length)
    { case 0:
        return call(aux1, f(), k) // nullary continuation
      case 1:
        return call
          ( aux1
          , exprs[0]
          , x => call(aux1, f(x), k) // unary
          )
      case 2:
        return call
          ( aux1
          , exprs[0]
          , x =>
            call
              ( aux1
              , exprs[1]
              , y => call(aux1, f(x, y), k) // binary
              )
          )
      case 3: // ternary ...
      case 4: // quaternary ...
      default: // variadic
        return call
          ( exprs.reduce
              ( (mr, e) =>
                  k => call(mr, r => call(aux1, e, x => call(k, [ ...r, x ])))
              , k => call(k, [])
              )
          , values => call(aux1, f(...values), k)
          )
    }
  }

  return trampoline(aux1(f()))
}

// trampoline : * -> *
const trampoline = r =>
{ while (r && r.type === call)
    r = r.f(...r.values)
  return r
}

// simpleGrid : ((...int -> 'a), int, int) -> 'a array
const simpleGrid = (f, dim1 = 0, dim2 = 0) =>
  reset
    ( call
        ( f
        , amb(range(0, dim1))
        , amb(range(0, dim2))
        )
    )

// multiGrid : (...int -> 'a, ...int) -> 'a array
const multiGrid = (f = always([]), ...dims) =>
  reset
    ( apply
        ( f
        , dims.map(_ => amb(range(0, _)))
        )
    )

// : unit
log
  ( "simple grid:"
  , simpleGrid((x, y) => [[x, y]], 3, 3)
  )

// : unit
log
  ( "multiGrid:"
  , multiGrid
      ( (...all) => [ all.map(line(2, 3)) ]
      , 3
      , 3
      , 3
      )
  )
2 голосов
/ 27 апреля 2020

Сначала, читая ваш код, я подумал, что вы сгенерировали один стиль сетки, так что makeGrid (7, 9) приведет к чему-то вроде этого:

[
  [[3, 3], [3, 5], [3, 7], [3, 9]], 
  [[5, 3], [5, 5], [5, 7], [5, 9]], 
  [[7, 3], [7, 5], [7, 7], [7, 9]]
]

Вместо этого он возвращает один массив пар :

[[3, 3], [3, 5], [3, 7], [3, 9], [5, 3], [5, 5], [5, 7], [5, 9], [7, 3], [7, 5], [7, 7], [7, 9]]

Я почти уверен, что я не единственный. Берги предложил исправить в комментариях, чтобы заменить его на прежний. (Это то, что могло бы изменить grid =[...grid, ...row] на grid =[...grid, row].) И замечательный ответ от Thankyou основан на том же предположении.

Это проблема.

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

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

Преимущество в удобочитаемости и понятности, однако, является ключом, а не фактом, что решение является рекурсивным.

Не поймите меня неправильно, рекурсия - одно из моих любимых программ. методы. Ответ от Thankyou красивый и элегантный. Но это не единственная техника, которая исправит проблемы, которые присутствуют в явных for -петлях. Обычно одна из первых вещей, которую я делаю, когда пытаюсь перевести младшего программиста на средний и более поздний уровень, - это заменить for -loops на более значимые конструкции. Большинство циклов пытаются сделать одну из нескольких вещей. Они пытаются преобразовать каждый элемент списка во что-то новое (map), пытаются выбрать какое-то важное подмножество элементов (filter), пытаются найти первый важный элемент (find) или пытаются объединить все элементы в одно значение (reduce). Используя их вместо этого, код становится более явным.

Также важно, как видно из ответа от Thankyou, разделять повторно используемые фрагменты кода, чтобы ваша основная функция могла сосредоточиться на важных частях. В приведенной ниже версии извлекается функция rangeBy, которая добавляет параметр step к моей обычной функции range. range создает диапазон целых чисел, так что, например, range (3, 12) приводит к [3, 4, 5, 6, 7, 8, 9, 10, 11, 12] rangeBy, добавляет начальный параметр step, так что range (2) (3, 12) дает [3, 5, 7, 9, 11].

Мы используем что rangeBy функционирует вместе с map и его кузеном flatMap, чтобы сделать более явную версию вашей зацикленной функции:

const rangeBy = (step) => (lo, hi) => 
  [... Array (Math .ceil ((hi - lo + 1) / step))] 
    .map ((_, i) => i * step  + lo)

const createGrid = (rows, columns) => 
  rangeBy (2) (3, rows) .flatMap (y => 
    rangeBy (2) (3, columns) .map (x => 
      [y, x]
    )
  )

console .log (createGrid (7, 9))

Зная, что делает rangeBy, мы можем мысленно прочитать это как

const createGrid = (rows, columns) => 
  [3, 5, 7, ..., rows] .flatMap (y => 
    [3, 5, 7, ..., columns] .map (x => 
      [y, x]
    )
  )

Обратите внимание, что если вы хотите поведение, которое я ожидал, вы можете достигните этого, просто заменив flatMap на map в createGrid. Кроме того, если вы сделаете это, добавьте более общее поведение c, которое предлагает Thankyou, тривиально, заменив [y, x] на f (x, y) и передав f в качестве параметра. Что остается жестко запрограммированным в этой версии, так это преобразование rows и columns в массивы нечетных чисел, начиная с 3. Мы могли бы сделать фактические массивы аргументами нашей функции и применить rangeBy снаружи. Но в этот момент мы, вероятно, рассматриваем другую функцию с идеальным названием cartesianProduct.


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

Обновление

Я хотел упомянуть об этом изначально и просто забыл. Следующая версия демонстрирует, что каррирование в rangeBy далеко от фундаментального. Мы можем легко использовать один звонок:

const rangeBy = (step, lo, hi) => 
  [... Array (Math .ceil ((hi - lo + 1) / step))] 
    .map ((_, i) => i * step  + lo)

const createGrid = (rows, columns) => 
  rangeBy (2, 3, rows) .flatMap (y => 
    rangeBy (2, 3, columns) .map (x => 
      [y, x]
    )
  )

console .log (createGrid (7, 9))

Основное обоснование для карри rangeBy заключается в том, что когда оно написано так:

const rangeBy = (step) => (lo, hi) => 
  [... Array (Math .ceil ((hi - lo + 1) / step))] 
    .map ((_, i) => i * step  + lo)

мы можем написать более распространенное range: просто применив 1 к вышесказанному. То есть

const range = rangeBy (1)

range (3, 12) //=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

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

2 голосов
/ 27 апреля 2020

Функциональное программирование больше относится к функциям высшего порядка, чем к прямой рекурсии. Я полагаю, что следующее эквивалентно вашему примеру, используя _.range из нижнего подчеркивания. js и map и flatMap из стандартной библиотеки.

const rowRange = _.range(3, rows + 1, 2);
const colRange = _.range(3, columns + 1, 2);
return rowRange.flatMap(row => colRange.map(col => [col, row]));
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...