【问题标题】:How do I replace for loops with recursion in Javascript?如何用 Javascript 中的递归替换 for 循环?
【发布时间】:2020-08-10 07:30:43
【问题描述】:

我有 2 个for 循环可以很好地创建一个包含行和列的网格,但我想使用递归来改进解决方案,因为它更简洁且推荐使用(在函数式编程中)。

所需的输出是 css 网格中使用的单个对数组

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
}

是否还有关于如何在可能的情况下将 for 循环转换为递归解决方案的指南?

【问题讨论】:

  • 除非您需要任意数量的维度,否则您的代码完全可以。
  • 您是如何得出使用递归而不是循环“更简洁、更推荐”这一结论的?您还有其他将循环转换为递归并改进代码的示例吗?
  • 使用递归并不总是“更简洁和推荐” - 我认为您试图将错误的概念应用于您的问题。这完全没问题。
  • 如果没有完整的上下文,很难判断这是否是一个有效的推荐,因为我不知道你在读什么书,但只有在有意义时才应用递归。 ,您不应该用递归解决方案替换每个for loop - 使用更简单、更易于阅读和理解的解决方案 - 这是最佳解决方案 解决您要解决的任何问题。
  • @letele 如果只是学习经验:尝试将每个循环转换为单独的递归函数。或者可能先将内部循环重构为一个单独的函数。

标签: javascript recursion functional-programming grid-layout


【解决方案1】:

原始递归

这是使用递归创建网格的一种可能方法 -

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 ]
//   ]
// ]

工作更聪明,而不是更努力

根据 Bergi 的评论,您可以通过使用 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]]

了解详情

正如其他人所展示的,这个使用flatMap 的特定版本的makeGrid 正在有效地计算cartesian product。等到您熟悉 flatMap 时,您已经知道 List Monad!


请多吃点蛋糕!

如果您想了解更多,我想向您介绍我在计算研究中最喜欢的主题之一:定界延续。开始使用一流的延续需要对它们的某些使用方式产生直觉 -

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 Monad 一样,上面的amb 封装了这种模糊(非确定性)计算的概念。我们可以使用定界延续轻松编写二维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]]

感谢amb,创建 N 维网格也变得轻而易举。实现几乎消失了 -

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]
// ]

那么resetcallapplyamb 来自哪里? JavaScript 不支持第一类延续,但没有什么能阻止我们implementing them on our own -

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 没有这些功能怎么办? Read the post 看看我们如何只使用普通函数来制作这些(以及更多)。


延续代码演示

在您自己的浏览器中查看它!展开下面的 sn-p 以在 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
      )
  )

【讨论】:

  • 我会 curry f(并从 makeRow 中省略 row 参数),并使用 rowCount/columnCount 而不是 rows/cols。跨度>
  • 请注意,这解决了(对我来说更有趣的问题)创建一个完整的 m x n 元素数组,这是 Bergi 建议的可能想要但被拒绝的东西。原来的只是创建一个对数组。
  • 谢谢。这个答案给了我比我预期的更多。我有递归的想法,但现在你介绍了柯里化函数和部分应用程序。这些概念听起来很有趣。
  • 请注意,所需的输出是一对数组。但我认为答案中使用的技术大大揭开了递归的神秘面纱。
  • @letele 感谢您的评论,我提供了另一个更新,表明您可以使结果变平。只需将map 替换为flatMap
【解决方案2】:

起初,在阅读您的代码时,我以为您生成了一种网格样式,因此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]]

我很确定我不是唯一一个。 Bergi 建议在 cmets 中进行修复以将其更改为前者。 (这就是将 grid =[...grid, ...row] 更改为 grid =[...grid, row] 会做的事情。)Thankyou 的精彩回答基于相同的假设。

这是个问题。

当读者无法快速理解您的代码的作用时,维护起来就会变得更加困难……即使是几周后的您自己也是如此。

您可能会听到用递归替换循环的建议的原因与此有关。循环都是关于明确的命令式指令来获得你想要的东西,这取决于变异变量,然后你必须跟踪这些变量,并且很容易受到错误的影响。递归通常更具声明性,一种说法是您正在寻找的结果只是将这些更简单的结果与我们当前的数据相结合,并指出如何通过基本案例或递归获得更简单的结果打电话。

不过,可读性和可理解性的优势是关键,而不是解决方案是递归的。

不要误会,递归是我最喜欢的编程技术之一。谢谢你的回答美丽而优雅。但这并不是解决显式for-loops 存在的问题的唯一技术。通常,当我试图将初级程序员提升到中级以上时,我做的第一件事就是用更有意义的构造替换for-loops。大多数循环都试图做一些事情之一。他们试图将列表中的每个元素转换为新的东西(map),试图选择元素的一些重要子集(filter),试图找到第一个重要元素(find),或者尝试将所有元素组合成一个值 (reduce)。通过使用这些,代码变得更加明确。

正如Thankyou 的回答中所见,同样重要的是拆分可重用的代码片段,以便您的主要功能可以专注于重要部分。下面的版本提取了一个函数rangeBy,它为我常用的range函数添加了一个step参数。 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]
    )
  )

请注意,如果您想要我所期望的行为,您只需将createGrid 中的flatMap 替换为map 即可实现。此外,如果您这样做,通过将 [y, x] 替换为 f (x, y) 并将 f 作为参数传递,添加Thankyou 提供的更通用的行为是微不足道的。在这个版本中仍然硬编码的是将 rowscolumns 转换为以 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))

currying rangeBy 的主要理由是当它这样写时:

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

我们可以通过简单地将1 应用到上面来编写更常见的range。也就是说,

const range = rangeBy (1)

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

这非常有用,以至于它已成为我编写函数的常用风格。但这不是简化问题的重要部分。

【讨论】:

  • 好的。但请注意,所需的输出是单个数组对。我打乱这个数组的结果,然后取数组的前 n 个元素,就像一个随机样本。然后我使用这个样本并使用这些对使用 css 网格来定位元素。本质上,我正在创建一个动态维恩图。
  • 对,这就是提供的版本所做的。您可以通过将flatMap 替换为map 来更改为我认为我正在阅读的版本。
  • 好的。柯里化是一个新概念,所以我需要时间来理解它。但我会调整 rangeBy 并尝试理解和实现它
  • 已更新以表明柯里化很有用但不是必需的。
  • 是的。我不介意柯里化并理解它简化了所涉及的论点。但解决方案确实是我正在寻找的,而不是递归。函数式编程的难点在于适应编写代码的风格,该风格声明在代码上应该发生什么,并描述了如何完成任务。 rangeBy 声明应该发生什么,而 for 循环解释如何创建网格的控制流程。
【解决方案3】:

函数式编程更多的是关于高阶函数而不是直接递归。我相信以下内容等同于您的示例,使用来自 underscore.js 的 _.range 和来自标准库的 mapflatMap

const rowRange = _.range(3, rows + 1, 2);
const colRange = _.range(3, columns + 1, 2);
return rowRange.flatMap(row => colRange.map(col => [col, row]));

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-11
    • 2013-02-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-03
    • 2021-04-02
    相关资源
    最近更新 更多