【问题标题】:Functional Programming style growing array loop函数式编程风格的增长数组循环
【发布时间】:2019-09-19 02:18:39
【问题描述】:

在 javascript 中迭代在迭代期间增长的数组的最聪明的方法是什么?我想遍历所有添加的元素,甚至在迭代期间添加的元素。我想以函数式编程风格来做。

例如看这段代码

let a = [
	'x',
  'y'
]

let limit = 4  // limit the test

for (let i=0; i<a.length; i++) {
	console.log(a[i])
    limit--
    if(limit>0){
      a.push(a[i]+'-') 
    }
}

console.log(a)

执行后得到

[
  "x",
  "y",
  "x-",
  "y-",
  "x--"
]

但如果我尝试使用像 forEach 这样的替代“功能”模式,则不会打印新添加的元素

let a = [
	'x',
  'y'
]

let limit = 4  // limit the test

a.forEach( (e) => {
    console.log(e)
    limit--
    if(limit>0){
      a.push(e+'-') 
    }
})

console.log(a)

【问题讨论】:

  • 第一个循环检查每次迭代的长度。随着长度的增长,迭代继续。使用forEach() 只迭代原始数组长度
  • a.forEach( (e) ) 说:对于名为 e 的 a 的每个元素,做:/:你的代码,这不是一个范围
  • “在 javascript 中迭代一个在迭代过程中增长的数组的最聪明的方法是什么?” 从函数的角度来看:迭代一个不会增长的数组。
  • 如果我不更深入地重构代码以防止数组在循环期间增长,那么 for(
  • 在函数式编程中,数组不会增长。不过,人们可能会使用惰性值流。

标签: javascript arrays functional-programming


【解决方案1】:

正如 Rubens 正确地注意到的那样,函数式方法假设您的函数不会改变源数据,而是转换它并返回新值。

在以下示例中,源数组保持不变,transform 通过递归调用自身并传递更新的参数来迭代它:

function transform(array, limit, acc = []) {
    if (limit === 0 || array.length === 0) {
        return acc;
    }

    const head = array[0];
    const tail = array.slice(1);
    return transform(tail.concat(head + "-"), limit - 1, acc.concat(head));
}

transform(["x", "y"], 5).forEach(x => console.log(x))

结果数组受 limit 参数限制,limit = 5 表示结果中只有 5 个元素。

如果我们查看在每次迭代中传递给transform 函数的参数,我们会发现limit 变得更小,而accaccumulator 的缩写)接收新元素:

transform(["x",   "y"],    5, [])
transform(["y",   "x-"],   4, ["x"])
transform(["x-",  "y-"],   3, ["x", "y"])
transform(["y-",  "x--"],  2, ["x", "y", "x-"])
transform(["x--", "y--"],  1, ["x", "y", "x-", "y-"])
transform(["y--", "x---"], 0, ["x", "y", "x-", "y-", "x--"])

limit 达到零时,acc 值作为结果返回。请注意,没有数据发生变异。

值得一提的是,transform 调用处于尾部位置,并且由于 ES6 optimizes tail calls(在严格模式下),我们不必担心调用堆栈的增长。

还要注意,上面的代码只是说明了这个想法。应该小心使用它,因为在每次迭代中都有sliceing 和concating 数组,因此它在处理大量数据时可能会非常慢。

更新关于尾调用优化:虽然尾调用优化是 ES6 规范的一部分,但浏览器并未广泛支持,因此在使用递归时需要谨慎。见compatibility table

【讨论】:

    【解决方案2】:

    函数式的方式,最好的方法是使用 Observables(反应式编程)。这个数组可以变成一个“生产者”(Observable),只要这个集合增长就会发出值,所以即使这个集合随着时间的推移而增长,你也可以安全地做任何你想做的事情。 也许这只是“记录”一些值的“过度工程”,但根据您的实际问题,它可能是一个很好的解决方案。

    您可以查看文档here

    但是,如果您查看您的代码,在 函数式 编程中,您不会更改当前数组(使用引用),而是创建一个全新的数组。在函数式编程中,变量往往是“只读的”,遵循“不变性”的概念。 JS 还没有原生,you can use libs for that tough。 无论如何,我已经将此代码作为示例:

    function dashAccumulator(arr, times) {
      if(times) {
        const dashed = arr.map(val => `${val}-`);
        return [...arr, ...dashAccumulator(dashed, --times)];
      }
      else return arr;
    }
    
    const a = ['x', 'y'];
    const iter = 4;
    console.log(dashAccumulator(a, iter));

    ma​​p 将创建一个新数组,并且在返回时,我将根据地图结果的值和该 context。这样您就不需要更改任何引用并保持相同的最终结果。

    【讨论】:

      【解决方案3】:

      使用通用的looprecur 接口,我们编写以函数式风格表示的堆栈安全循环 -

      const recur = (...values) =>
        ({ recur, values })
      
      const loop = f =>
      { let r = f ()
        while (r && r.recur === recur)
          r = f (...r.values)
        return r
      }
      
      const main = (limit = 10) =>
        loop                    // begin a loop with vars
          ( ( r = [ 'x', 'y' ]  // initial result
            , i = 0             // initial index
            ) =>
              i >= limit           // exit condition
                ? r .slice (0, i)  // return result
                : recur            // otherwise recur
                    ( [ ...r, r[i] + '-' ]  // next result
                    , i + 1                 // next index
                    )
          )
      
      console .log (main (10))
      // ["x","y","x-","y-","x--","y--","x---","y---","x----","y----"]

      我们也可以轻松地将[ 'x', 'y' ] 作为程序的输入 -

      const main = (init = [], limit = 10) =>
        loop
          ( ( r = [...init]    // <-- initial result
            , i = 0
            ) =>
              i >= limit
                ? r .slice (0, i)
                : recur
                    ( [ ...r, r[i] + '-' ]
                    , i + 1
                    )
          )
      
      main ([ 'a', 'b', 'c' ], 10)
      // ["a","b","c","a-","b-","c-","a--","b--","c--","a---"]
      

      在上面,我们在循环运行时使用索引i = 0 来执行查找r[i],但这只是解决问题的一种方法。要查看 looprecur 以不同方式构建数组,我们将看到它们在另一个场景中的使用。

      让我们想象一个游戏,您掷出N 面的骰子,结果M 小于N。如果M0,则游戏结束,否则记录您的掷骰并用M 面骰子重复游戏-

      const rand = max =>
        Math .floor (Math .random () * max)
      
      const play = (init = 0) =>
        loop                     // begin a loop with vars
          ( ( r = []             // initial result
            , max = init         // initial max
            , roll = rand (init) // initial roll
            ) =>
              roll === 0              // gameover condition
                ? [ ...r, roll ]      // return result
                : recur               // otherwise recur
                    ( [ ...r, roll ]  // next result
                    , roll            // next max
                    , rand (roll)     // next roll
                    )
          )
      
      console .log (JSON .stringify (play (1000)))
      // [688,416,215,12,5,1,0]
      

      在这个特定的程序中,我们的大脑不再考虑数组索引或递增它们。上面,我们在构建它时不会读取结果r。相反,我们使用另一个循环参数max 来编码当前滚动限制。 looprecur 足够通用,可以表达各种功能程序。


      在这两个程序中,传播参数,如 [ ...r, r[i] + '-' ][ ...r, roll ],在每个步骤中复制结果 r。由于r 在循环开始时使用一个新数组r = [...] 进行了初始化,因此我们可以使用变异操作Array.prototype.push,而不会有泄漏副作用的风险。这大大减少了运行时间和内存占用 -

      const push = (xs, x) =>
        ( xs .push (x) // <-- perform side-effect
        , xs           // <-- return value
        )
      
      const play = (init = 0) =>
        loop
          ( ( r = []             // <-- new array
            , max = init
            , roll = rand (init)
            ) =>
              roll === 0
                ? push (r, roll)      // <-- mutate
                : recur
                    ( push (r, roll)  // <-- mutate
                    , roll
                    , rand (roll)
                    )
          )
      

      将下面的sn-p扩展为play游戏与N = 1000-

      const recur = (...values) =>
        ({ recur, values })
      
      const loop = f =>
      { let r = f ()
        while (r && r.recur === recur)
          r = f (...r.values)
        return r
      }
      
      const rand = max =>
        Math .floor (Math .random () * max)
      
      const push = (xs, x) =>
        ( xs .push (x)
        , xs
        )
      
      const play = (init = 0) =>
        loop
          ( ( r = []
            , max = init
            , roll = rand (init)
            ) =>
              roll === 0
                ? push (r, roll)
                : recur
                    ( push (r, roll)
                    , roll
                    , rand (roll)
                    )
          )
      
      console .log (JSON .stringify (play (1000)))
      // [688,416,215,12,5,1,0]

      【讨论】:

        【解决方案4】:

        我想以函数式编程风格来做。

        在函数式编程中,数组不会增长。但是,使用惰性,您可以迭代尚不存在的元素,基本上是无限流。例如,在 Haskell 中:

        > let a = ["x", "y"] ++ map (++"-") a
        > --                                ^ reference to the list itself
        > take 5 a
        ["x","y","x-","y-","x--"
        

        在 javascript 中迭代一个在迭代过程中增长的数组的最聪明的方法是什么?

        我会使用类似的方法,当然你需要一个帮助函数来改变数组:

        function takeMapPrev(fn, arr, limit) {
            if (!arr.length) throw new RangeError("must start with something");
            var i = 0;
            while (arr.length < limit)
                arr.push(fn(arr[i++]));
            return arr;
        }
        
        console.log(takeMapPrev(e => e+'-', ['x','y'], 5)); // ["x","y","x-","y-","x--"]
        

        【讨论】:

          猜你喜欢
          • 2016-11-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-11-17
          • 2016-08-27
          • 2010-11-08
          • 1970-01-01
          相关资源
          最近更新 更多