Javascript中具有递归的高阶函数
Posted
技术标签:
【中文标题】Javascript中具有递归的高阶函数【英文标题】:Higher-order function with recursion in Javascript 【发布时间】:2021-11-01 07:09:08 【问题描述】:这里是新手...我试图掌握 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?
任何指导、帮助或指出我正确的方向将不胜感激。
【问题讨论】:
为什么要使用递归来展平数组?你想让你的函数能够处理[[[[[[[[[[34]]]]]]]]]]
吗?
你要么需要args.map(apply2Array(fn))
,要么使用部分应用的函数表达式(虽然不能使用简洁的箭头语法)。
【参考方案1】:
如果你的数据结构不是任意嵌套的,你也可以考虑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));
【讨论】:
【参考方案2】:我对这里的答案有点困惑。我不知道他们是否在响应我实际上没有看到的要求,或者我是否遗漏了一些重要的东西。
但是,如果您只是想要一个装饰器,它将标量上的函数转换为对标量或标量数组进行操作的装饰器,那么这很简单,而且您离我们不远了。应该这样做:
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
,可能与制图有关。
其他 cmets 和答案建议您也希望在嵌套数组上使用相同的方法,将 [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 的创始人和主要作者。 Ramda 有一个与此相关的map
函数。但它似乎可以对多种类型进行操作,包括数组、对象、函数等等。这不是矛盾吗?
我会说不。我们只需要向上移动一个抽象层。 FantasyLand 指定一个抽象泛型类型,Functor(借用抽象数学)。这些类型以某种方式包含另一种类型的一个或多个值,我们可以通过map
ping 提供给这些值中的每一个的函数来创建类似结构的容器。您的map
函数必须遵守某些简单的规则才能将其视为 Functor,但如果您这样做,那么 Ramda 的 map
将与您的类型正常工作。换句话说,Ramda 的map
不适用于数组,但适用于任何 Functor。 Ramda 本身为数组、对象和函数提供了实现,但将对其他类型的调用委托给它们自己的 map
方法。
不过,基本的一点是,Ramda 在这里并没有真正增加额外的复杂性,因为 Ramda 的 map
的输入类型是 Functor
而不是 Array
。
简单
函数式编程涉及很多方面。但中心主题之一必须是简单性。如果你还没有看过 Rich Hickey 的演讲 Simple Made Easy,我强烈推荐它。它解释了简单的客观概念,并描述了如何实现它。
【讨论】:
【参考方案3】: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]))
【讨论】:
看起来更好,谢谢,但我会删除apply3Array
,因为它与问题无关,并给apply4Array
一个更清晰的名称来描述其目的。此外,如果args
恰好是一个带有map
键的对象,args.map
将失败。最好使用Array.isArray
。
感谢更新名称,我想保留 'apply3Array'/'recursiveLoop' 以便有人可以查看增量更新以达到最终解决方案,
哦,是的,我错过了它与 OP 的尝试不同。
您可以(也应该)将c => genericRecursiveLoop(func)(c)
简化为genericRecursiveLoop(func)
(使用η 约简)【参考方案4】:
如果给内部函数起个名字,写递归就更容易了:
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]));
【讨论】:
以上是关于Javascript中具有递归的高阶函数的主要内容,如果未能解决你的问题,请参考以下文章