Javascript 递归的高阶函数

Higher-order function with recursion in Javascript

这里是新手...我试图掌握 Javascript 中函数式编程的概念,但我卡住了。

我正在尝试通过递归(高阶函数)将一个函数应用于另一个函数。假设我有一个可以是变量或数组的输入,例如:

const A = [5, 14, 23, 32, 41];
const B = 50;

我的基本功能应该是将华氏度转换为摄氏度(但它实际上可以是任何函数)

const convertF2C = x => (x - 32) / 1.8;

所以我通常的解决方法是:

const result = array => array.map ? array.map(result) : convertF2C(array); // using recursion if the input is an array

上面的问题是,如果我想更改“结果”函数中的convertF2C,我将不得不修改代码

所以,从功能上考虑,我应该能够创建一个采用基本功能的通用功能,如下所示:

const arrayResult = apply2Array(convertF2C);

console.log(arrayResult(A)); // Output: [-15, -10, -5, 0, 5]
console.log(arrayResult(B)); // Output: 10

我猜测通用函数“apply2Array”应该类似于:

const apply2Array = fn => (...args) => args.map ? args.map(apply2Array) : fn(...args); // does not work

我在这里发现了一个“有点”类似的问题,但对我没有帮助:Higher-order function of recursive functions?

非常感谢任何指导、帮助或指出正确的方向。

genericRecursiveLoop应该是您要找的答案。

const convertF2C = x => (x - 32) / 1.8;

// in case you really want to use recursion 
// args[0].map is actually just [[1,2,3]][0].map will return try if nested Array
// insuch a case you flaten them manuallu ...arg[0] is extracting nested array
const recursiveLoop = (args) => args.map ? args.map(recursiveLoop) : convertF2C(args)

console.log('recursiveLoop')
console.log(recursiveLoop(5))
console.log(recursiveLoop(6))
console.log(recursiveLoop(5, 6)) // it will only show response for 5 
console.log(recursiveLoop([5, 6]))

/* ANSWER */
const genericRecursiveLoop = (func) =>  (args) => args.map ?  args.map(genericRecursiveLoop(func)) : func(args);

let innerRecursiveLoopFunc = genericRecursiveLoop(convertF2C)
console.log('genericRecursiveLoop')
console.log(innerRecursiveLoopFunc(5))
console.log(innerRecursiveLoopFunc(6))
console.log(innerRecursiveLoopFunc(5, 6)) // it will only show response for 5 
console.log(innerRecursiveLoopFunc([5, 6]))

如果给内部函数起个名字,写递归就容易多了:

const makeDeeplyMappable = fn => function deepMap(a) {
  return Array.isArray(a) ? a.map(deepMap) : fn(a);
};

const convertF2C = x => (x - 32) / 1.8;

const deepF2C = makeDeeplyMappable(convertF2C);
console.log(deepF2C([[32, [0]]]));
console.log(deepF2C([[[[5]]], 32]));

我对这里的答案有点困惑。我不知道他们是否在响应我实际上没有看到的要求,或者我是否遗漏了一些重要的东西。

但是,如果您只想要一个装饰器,将标量上的函数转换为对标量或标量数组进行操作的函数,那非常简单,而且您离得并不远。应该这样做:

const apply2Array = (fn) => (arg) => 
  Array .isArray (arg) ? arg .map (fn) : fn (arg)

const convertF2C = (t) => (t - 32) / 1.8

const A = [5, 14, 23, 32, 41]
const B = 50

const arrayResult = apply2Array(convertF2C);

console .log (arrayResult (A))
console .log (arrayResult (B))
.as-console-wrapper {max-height: 100% !important; top: 0}

我建议您应该使用 Array.isArray 进行检查,而不是 map 属性 的存在。名为 map 的 属性 可能不是 Array.prototype.map,可能与制图有关。

其他评论和答案表明您也想对嵌套数组进行相同的操作,将 [5, [[14, 23], 32], 41] 之类的内容转换为 [-15, [[-10, -5], 0], 5]。那不会更难。正如 Bergi 所建议的,您需要做的就是将递归应用的函数包装在同一个装饰器中:

const apply2Array = (fn) => (arg) => 
  Array .isArray (arg) ? arg .map (apply2Array (fn)) : fn (arg)
  //                               ^^^^^^^^^^^
const convertF2C = (t) => (t - 32) / 1.8

const A = [5, 14, 23, 32, 41]
const B = 50
const C = [5, [[14, 23], 32], 41]

const arrayResult = apply2Array(convertF2C);

console .log (arrayResult (A))
console .log (arrayResult (B))
console .log (arrayResult (C))
.as-console-wrapper {max-height: 100% !important; top: 0}

不要这样做

不过,我还是建议这个企业充满潜在的陷阱。想象一下,例如,您有一个对数字数组进行操作的 sum 函数,并且您希望使用它对数字数组或数字数组进行操作。

如果您使用任一版本的 apply2Array 将其包装起来,它将无法正常工作。对于第一个版本,如果您提供一个数字数组,该函数将按预期工作,但如果您只提供一个数字数组,该函数将失败。第二个无论如何都会失败。

问题在于,有时您的基本函数 想要 对数组进行操作。制作一个根据其输入类型执行多项操作的函数会使您失去一些简单性。

相反,我建议您创建多个函数来完成您需要的不同事情。您仍然可以使用装饰器,但比上面的更通用。

这里我们使用一个叫做map,具体化Array.prototype.map:

const map = (fn) => (xs) => 
  xs .map (x => fn (x))

const convertF2C = (t) => (t - 32) / 1.8
const convertAllF2C = map (convertF2C)

const A = [5, 14, 23, 32, 41]
const B = 50

console .log (convertAllF2C (A))
console .log (convertF2C (B))
.as-console-wrapper {max-height: 100% !important; top: 0}

如果你也想要深度映射,你可以重命名上面的装饰器,然后这样做:

const map = (fn) => (xs) => 
  xs .map (x => fn(x))
const deepMap = (fn) => (arg) => 
  Array .isArray (arg) ? arg .map (deepMap (fn)) : fn (arg)

const convertF2C = (t) => (t - 32) / 1.8
const convertAllF2C = map (convertF2C)
const deepConvertF2C = deepMap (convertF2C)

const A = [5, 14, 23, 32, 41]
const B = 50
const C = [5, [[14, 23], 32], 41]

const arrayResult = deepMap (convertF2C);

console .log (convertAllF2C (A))
console .log (convertF2C (B))
console .log (deepConvertF2C (C))
.as-console-wrapper {max-height: 100% !important; top: 0}

拥有三个单独的函数来调用您的三种情况通常比一个函数更简单,该函数可以用三种不同类型的输入和三种不同类型的输出相关联来调用。由于这些是从我们的基本函数构建的,只有一些通用装饰器,它们仍然很容易维护。

但这不是与...矛盾吗?

有些人知道我是与此相关的 Ramda. And Ramda has a map 功能的创始人和主要作者。但它似乎可以对多种类型进行操作,包括数组、对象、函数等等。这不是自相矛盾吗?

我会说不。我们只需要向上移动一个抽象层。 FantasyLand specifies an abstract generic type, Functor(借自抽象数学)。这些类型以某种方式包含另一种类型的一个或多个值,我们可以通过 mapping 提供给每个值的函数来创建一个类似结构的容器。您的 map 函数必须遵守某些简单的规则才能被视为 Functor,但如果您这样做,那么 Ramda 的 map 将与您的类型一起工作。换句话说,Ramda 的 map 并不专门作用于数组,而是作用于任何 Functor。 Ramda 本身提供数组、对象和函数的实现,但将对其他类型的调用委托给它们自己的 map 方法。

不过,最基本的一点是,Ramda 并没有真正在此处增加额外的复杂性,因为 Ramda 的 map 的输入类型是 Functor 而不是 Array.

简单

函数式编程涉及很多方面。但中心主题之一必须是简单性。如果您还没有看过 Rich Hickey 的演讲 Simple Made Easy,我强烈推荐它。它解释了 objective 简单的概念,并描述了如何实现它。

如果您的数据结构不是任意嵌套的,您也可以考虑 Array#flatMap...这可能会帮助您保持可读性。

const A = [5, 14, 23, 32, 41];
const B = 50;

const toCelsius = (x) => []
  .concat(x)
  .flatMap((n) => (n - 32) / 1.8)
;

console.log('A', toCelsius(A));
console.log('B', toCelsius(B));