【问题标题】:Functional js - cant recursively call own function函数式 js - 不能递归调用自己的函数
【发布时间】:2016-04-05 17:08:45
【问题描述】:

在使用函数式编程原理从通用 reduce 函数生成 flatten 函数时,我无法从数组 flatten 函数中获取不正确的值。我相信这是因为调用中的递归存在问题,但我不确定如何通过它,因为工作和非工作函数的函数签名应该是相同的。

感谢您的帮助。

var data = [['one','two','three'], ['four', 'five', ['six']], 'seven', ['eight', 'nine']];

// here is an example of flatten that works perfectly. it takes an array and reduces 
// the internal arrays to a single flat array
function flatten( arr ){
  return arr.reduce(function( ret, curr ){
    if( Array.isArray( curr ) ){
      ret = ret.concat( flatten( curr ) );
    } else {
      ret.push( curr );
    }
    return ret;
  }, []);
}

// here is what I am trying to achieve. This one combines my reduction functon with the 
// functional `reduceWith` function. The code signature is exactly the same, however the
// end result is different.
// `functionalFlatten` does resolve to the correct function inside
var functionalFlatten = reduceWith(function( ret, curr ){
  if( Array.isArray( curr ) ){
    ret = ret.concat( functionalFlatten( curr ) );
  } else {
    ret.push( curr );
  }
  return ret;
}, []);

// this function will return a functional reduction function 
function reduceWith( fn, initial ) {
  return function _reduceWith( arr ) {
    return Array.prototype.reduce.call(arr, fn, initial || []);
  }
}

console.log('data', data);
console.log('functionalFlatten', functionalFlatten );
console.log('normal', flatten( data ));
console.log('fuctional', functionalFlatten( data ));
<script src="http://codepen.io/synthet1c/pen/WrQapG.js"></script>

【问题讨论】:

  • 你签出Ramda了吗?
  • 不,从来没有见过那个图书馆。会检查出来。感谢您的提醒。

标签: javascript recursion functional-programming


【解决方案1】:

在这里,您可以将reduceWith 写为柯里化函数,这样您可以更好地重用整个函数。

// ES6
const reduce = f => y => xs => xs.reduce (f, y);

现在我们可以将flatten写成部分应用的reduce函数

const flatten = reduce ((y,x) => y.concat (isArray (x) ? flatten (x) : x)) ([]);

那个isArray的小助手只是

const isArray = Array.isArray;

而且它确实有效。通过.push 没有突变,没有Function.prototype.call。只是折叠和连接。

console.log (flatten ([1,2,[3,4,[],6,[7,8,9]]]));
//=> [1,2,3,4,5,6,7,8,9]

这是 ES5

// ES5
"use strict";

var reduce = function reduce(f) {
  return function (y) {
    return function (xs) {
      return xs.reduce(f, y);
    };
  };
};

var isArray = Array.isArray;

var flatten = reduce(function (y, x) {
  return y.concat(isArray(x) ? flatten(x) : x);
})([]);

console.log(flatten([1, 2, [3, 4, [], 6, [7, 8, 9]]]));
//=> [1,2,3,4,5,6,7,8,9]

【讨论】:

  • 何 Naomik,感谢您的回复。我唯一不确定的是不使用Array.prototype.reduce.call 我这样做是为了可以传入任何像对象这样的数组并减少/映射/过滤等。不过这可能是想多了。
  • @synthet1c:你能举个例子说明foo.reduce 什么时候不起作用,Array.prototype.reduce.call 什么时候起作用?
  • 当您使用类似数组的对象时,例如,参数var arrayLike = { 0: 'zero' , 1: 'one' , 2: 'two' , 3: 'three' , length: 4 }; someReductionFunction( arrayLike ); 如果您使用类似数组的对象,只要它具有数字索引键和长度属性,您可以从Array.prototype 借用方法跨度>
  • @synthet1c 我不会编写所有函数来支持多种数据类型,而是创建一个函数来将一种数据类型转换为另一种数据类型:toArray(arrayLike) //=> [...]arguments 是我在 JavaScript 中遇到的最常见的类数组,并且由于 rest parameters 而在 ES6 中消失了。 function foo (...xs) { doSomething(xs); } 而不是 function foo() { doSomething(arguments); } 我从不喜欢 arguments 的隐含行为,我很高兴这种变化即将到来。
  • @naomik 感谢您的跟进。我认为你已经提出了双赢。更少的代码,更干净。我喜欢。是的,我真的很期待 ...rest 而不是 slice(args[, startIndex ]),剩下的糖 es6+ 会提供。
【解决方案2】:

我对您的代码进行了一些更改。这有效:

var data = [
  ['one','two','three'],
  ['four', 'five', ['six']],
  'seven',
  ['eight', 'nine']
]

function flatten(arr) {
  return arr.reduce(function(ret, curr) {
    return ret.concat(Array.isArray(curr) ? flatten(curr) : [curr])
  }, [])
}

var functionalFlatten = reduceWith(function(ret, curr) {
  return Array.prototype.concat.call(ret, Array.isArray(curr) ? functionalFlatten(curr) : [curr])
}, [])

// I assume you want to use .call to keep it functional or what ever
// But I would just do it like this:
var _functionalFlatten = reduceWith(function(ret, curr) {
  return ret.concat(ret, Array.isArray(curr) ? functionalFlatten(curr) : [curr])
}, [])

function reduceWith(fn, initial) {
  return (function (arr) {
    return Array.prototype.reduce.call(arr, fn, initial) 
  })
}

// Again, keep it simple...
function _reduceWith(fn, initial) {
  return (function (arr) {
    return arr.reduce(fn, initial)
  })
}

// You had this...
function reduceWith( fn, initial ) {
  // You don't need to name this function:
  return function _reduceWith( arr ) {
    // Let's keep this in line original function, so remove the default:
    return Array.prototype.reduce.call(arr, fn, initial || []);
  }
}

console.log('data', data)
console.log('functionalFlatten', functionalFlatten)
console.log('normal', flatten(data))
console.log('fuctional', functionalFlatten(data))

现在到实际问题...

var functionalFlatten = reduceWith(function( ret, curr ){
  if( Array.isArray( curr ) ){
    ret = ret.concat( functionalFlatten( curr ) );
  } else {
    // This is your culprit:
    ret.push( curr ); // push will mutate ret
  }
  return ret;
}, []);
  • reduceWith 只被调用一次(当 functionalFlatten 被定义时)。
  • 每次都会调用内部函数
  • ...但ret.push(curr) 有可能改变initial

这是证据……

function reduceWithMutationSafe(fn, initial) {
  return (function (arr) {
    // Clone initial, so that the original can't be mutated:
    var clonedInitial = eval(JSON.stringify(initial))
    return arr.reduce(fn, clonedInitial)
  })
}

var functionalFlatten = reduceWithMutationSafe(function(ret, curr) {
  if(Array.isArray(curr)) {
    ret = ret.concat(functionalFlatten(curr))
  } else {
    ret.push(curr)
  }
  return ret
}, [])

这会起作用,即使functionalFlatten 与以前完全相同
ret.push(curr) 会改变克隆的initial,但原来的 不会被碰。

但最后一段代码只是证明。应该不用reduceWithMutationSafe

【讨论】:

  • 感谢您花时间浏览代码并指出可以改进的地方。
  • 再次感谢@Arnar。您提供的解释和证明非常有道理。
  • 我还推荐另一种检查变量是否为array 的方法。见:stackoverflow.com/questions/767486/…
【解决方案3】:

这是我修复你的功能的方法

var functionalFlatten = reduceWith(function f( ret, curr ){
  if( Array.isArray( curr ) ){
    ret = ret.concat( reduceWith(f, [])( curr ) );
  } else {
    ret.push( curr );
  }
  return ret;
}, []);

您的初始代码的问题是对父调用和递归调用使用相同的initial 值,这使得ret 与自身连接。

【讨论】:

  • 我不知道为什么你被否决了,但我赞成平衡——这是一个很好的答案。对于 OP,如果您将非空数组作为 functionalFlatteninitial 参数插入到其中,则更加明显,您可以看到每次遇到新数组时都会调用 functionalFlatten,因此重复模式你的结果。
  • @stefan 感谢您的回答。我不确定的是 functionalFlatten 应该在编译时创建,并且应该并且可用于内部函数。出于这个原因,我没有按照您的建议引用该函数。还看看这个,它似乎效率较低,因为您正在声明一个新的扁平化,每个级别的递归不重用相同的函数。
  • @Stefan:这不是问题的根源。 reduceWith 只被调用一次(当 functionalFlatten 被定义时)。每次都会调用内部函数,但ret.push(curr) 有可能改变initial
猜你喜欢
  • 1970-01-01
  • 2019-10-04
  • 1970-01-01
  • 2021-12-27
  • 2016-12-06
  • 1970-01-01
  • 2015-01-09
  • 1970-01-01
  • 2019-01-19
相关资源
最近更新 更多