美丽
深度函数是用递归表达得非常漂亮的函数之一——这个答案类似于 Nina 的答案,但说明了不同的推理路线。
const depth = ({ children = [] }) =>
children.length === 0
? 0 // base
: 1 + Math.max (...children.map (depth)) // inductive
首先我们解构传入节点,当属性未设置时分配children = []。这使我们可以使用数组的传统基础和归纳案例来解决问题:
-
基本情况:数组为空
-
归纳案例:数组非为空,因此我们有至少一个元素需要处理
Nina 的回答非常巧妙地避免了任何if 或三元?:!她通过偷偷将基本情况作为Math.max 的第一个参数来做到这一点!她太聪明了
这是一个有效的例子
const depth = ({ children = [] }) =>
children.length === 0
? 0
: 1 + Math.max (...children.map (depth))
const test =
{ name: 'level 0 item'
, children:
[ { name: 'level 1 item'
, children:
[ { name: 'level 2 item' }
, { name: 'second level 2 item'
, children:
[ { name: 'level 3 item' } ]
}
]
}
, { name: 'second level 1 item'
, children:
[ { name: 'level 2 item' }
, { name: 'second level 2 item'
, children:
[ { name: 'level 3 item'
, children:
[ { name: 'level 4 item' } ]
}
]
}
]
}
]
}
console.log (depth (test))
// 4
野兽
我们在上面使用了一些高级函数和语言实用程序。如果我们是这个概念的新手,在先学会低层次思考之前,我们无法实现更高层次的思考
-
Math.max 接受任意数量的参数。这究竟是如何工作的?
- 我们使用剩余参数语法
...children 将值数组转换为函数调用中的单个参数。这种转换究竟是如何工作的?
- 我们使用
Array.prototype 中的map 将我们的子节点数组转换为节点深度数组。这是如何运作的?我们真的需要创建一个 new 数组吗?
为了培养对这些内置功能和特性的理解,我们将研究如何自行实现这些结果。我们将重温depth,但这次我们将用我们自己的努力取代所有这些魔法
const depth = ({ children = [] }) =>
children.length === 0
? 0 // base
: 1 + magicWand (children) // inductive
现在我们只需要一根魔杖……首先,我们从一些基本的制作材料开始
const isEmpty = (xs = []) =>
xs.length === 0
const first = (xs = []) =>
xs [0]
const rest = (xs = []) =>
xs.slice (1)
我想继续思考 base 和 inductive 的情况,这些原始函数补充了这种推理。
让我们首先了解magicWand 将(必须)如何工作
// magicWand takes a list of nodes and must return a number
1 + magicWand (children)
那么让我们看看我们的两个案例
-
基本情况:输入列表
isEmpty,所以返回0——没有孩子,所以没有要添加的深度
-
归纳案例:列表有至少一个子项 - 计算
first 项目的深度,在 rest 上挥动魔杖,然后取出 @987654344 @ 这两个值中的一个
我们的魔杖完成了
const magicWand = (list = []) =>
isEmpty (list)
// base
? 0
// inductive
: max ( depth (first (list))
, magicWand (rest (list))
)
剩下的就是定义max
const max = (x = 0, y = 0) =>
x > y
? x
: y
只是为了确保此时一切仍然正常......
const max = (x = 0, y = 0) =>
x > y
? x
: y
const isEmpty = (xs = []) =>
xs.length === 0
const first = (xs = []) =>
xs [0]
const rest = (xs = []) =>
xs.slice (1)
const depth = ({ children = [] }) =>
children.length === 0
? 0 // base
: 1 + magicWand (children) // inductive
const magicWand = (list = []) =>
isEmpty (list)
// base
? 0
// inductive
: max ( depth (first (list))
, magicWand (rest (list))
)
const test =
{ name: 'level 0 item'
, children:
[ { name: 'level 1 item'
, children:
[ { name: 'level 2 item' }
, { name: 'second level 2 item'
, children:
[ { name: 'level 3 item' } ]
}
]
}
, { name: 'second level 1 item'
, children:
[ { name: 'level 2 item' }
, { name: 'second level 2 item'
, children:
[ { name: 'level 3 item'
, children:
[ { name: 'level 4 item' } ]
}
]
}
]
}
]
}
console.log (depth (test)) // 4
所以要实现更高层次的思考,你必须首先想象你的程序运行时会发生什么
const someList =
[ x, y, z ]
magicWand (someList)
// ???
x、y 和 z 是什么并不重要。您只需想象magicWand 将使用每个独立部分构建的函数调用堆栈。我们可以看到随着更多项目添加到输入列表中,这将如何扩展......
max ( depth (x)
, max ( depth (y)
, max ( depth (z)
, 0
)
)
)
当我们看到我们的函数构建的计算时,我们开始看到它们结构的相似之处。当一个模式出现时,我们可以在一个可重用的函数中捕捉到它的本质
在上面的计算中,max 和 magicWand 被硬编码到我们的程序中。如果我想用树计算不同的值,我需要一根完全不同的魔杖。
这个函数被称为 fold 因为它在一个可遍历的数据结构中的每个元素之间折叠一个用户提供的函数f。您将看到我们的标志性基础和归纳案例
const fold = (f, base, list) =>
isEmpty (list)
? base
: f ( fold ( f
, base
, rest (list)
)
, first (list)
)
现在我们可以使用我们的通用 fold 重写 magicWand
const magicWand = (list = []) =>
fold ( (acc, x) => max (acc, depth (x))
, 0
, list
)
magicWand 抽象不再需要。 fold可以直接在我们原来的函数中使用。
const depth = ({ children = [] }) =>
children.length === 0
? 0
: 1 + fold ( (acc, x) => max (acc, depth (x))
, 0
, children
)
当然,阅读原文要困难得多。语法糖为您提供了代码中的各种快捷方式。缺点是初学者经常觉得必须总是有某种甜蜜的"shorthand" 解决他们的问题 - 然后他们被困when it's just not there。
功能代码示例
const depth = ({ children = [] }) =>
isEmpty (children)
? 0
: 1 + fold ( (acc, x) => max (acc, depth (x))
, 0
, children
)
const fold = (f, base, list) =>
isEmpty (list)
? base
: f ( fold ( f
, base
, rest (list)
)
, first (list)
)
const max = (x = 0, y = 0) =>
x > y
? x
: y
const isEmpty = (xs = []) =>
xs.length === 0
const first = (xs = []) =>
xs [0]
const rest = (xs = []) =>
xs.slice (1)
const test =
{ name: 'level 0 item'
, children:
[ { name: 'level 1 item'
, children:
[ { name: 'level 2 item' }
, { name: 'second level 2 item'
, children:
[ { name: 'level 3 item' } ]
}
]
}
, { name: 'second level 1 item'
, children:
[ { name: 'level 2 item' }
, { name: 'second level 2 item'
, children:
[ { name: 'level 3 item'
, children:
[ { name: 'level 4 item' } ]
}
]
}
]
}
]
}
console.log (depth (test))
// 4
野兽模式
在depth 的最后一个实现中,我们看到了这个 lambda(匿名函数)表达式。
(acc, x) => max (acc, depth (x))
我们即将见证我们自己创造的一项令人难以置信的发明。这个小 lambda 非常有用,我们实际上会给它一个名字,但在我们能够利用它的真正力量之前,我们必须首先制作 max 和 depth 参数——我们已经制作了一个新的魔杖
const magicWand2 = (f, g) =>
(acc, x) => g (acc, f (x))
const depth = ({ children = [] }) =>
isEmpty (children)
? 0
: 1 + fold (magicWand2 (depth, max), 0, children)
// Tada!
乍一看,你认为这一定是有史以来最没用的魔杖!你可能会怀疑我是那些在一切都变成point-free 之前不会停止的僵尸之一。你吸气并暂停你的反应片刻
const concat = (xs, ys) =>
xs.concat (ys)
const map = (f, list) =>
fold (magicWand2 (f, concat), [], list)
map (x => x * x, [ 1, 2, 3, 4 ])
// => [ 16, 9, 4, 1 ]
诚然,我们认为这很酷。但我们不会被 2 把戏的小马眼花缭乱。明智的做法是阻止任何旧函数登陆您的程序或库,但如果忽略这一点,您将是个傻瓜。
const filter = (f, list) =>
fold ( magicWand2 (x => f (x) ? [ x ] : [], concat)
, []
, list
)
filter (x => x > 2, [ 1, 2, 3, 4 ])
// [ 4, 3 ]
好的,除了map 和filter 以“相反”的顺序组装结果之外,这根魔杖有一些严重的热量。我们称它为mapReduce,因为它给了我们两个参数,每个参数一个函数,并创建一个新的归约函数来插入fold
const mapReduce => (m, r) =>
(acc, x) => r (acc, m (x))
-
m,mapping 函数——这让你有机会在...之前转换传入的元素。
-
r,reducing 函数 - 该函数将累加器与映射元素的结果相结合
至于fold 在“反向”中组装结果,它不是。这恰好是一个右折叠。下面,我们可以将f 想象成一些二进制函数(即+)——参见前缀符号f (x y) 中的计算,而中缀符号x + y 应该有助于突出关键区别
foldR (f, base, [ x, y, z ])
// = f (f (f (base, z), y), x)
// = ((base + z) + y) + x
foldL (f, base, [ x, y, z ])
// = f (f (f (base, x), y), z)
// = (((base + x) + y) + z
现在让我们定义我们的左折叠,foldL - 我将fold 重命名为foldR 并将它放在这里,以便我们可以并排看到它们。
const foldL = (f, base, list) =>
isEmpty (list)
? base
: foldL ( f
, f (base, first (list))
, rest (list)
)
const foldR = (f, base, list) =>
isEmpty (list)
? base
: f ( foldR ( f
, base
, rest (list)
)
, first (list)
)
许多 JavaScript 开发人员不知道 reduceRight 存在于 Array.prototype 上。如果您只使用过 reduce 的交换函数,您将无法检测到差异。
好的,所以要修复我们的 map 和 filter,我们只需将 fold 绑定替换为 foldL
const map = (f, list) =>
foldL (mapReduce (f, concat), [], list)
const filter = (f, list) =>
foldL (mapReduce (x => f (x) ? [ x ] : [], concat), [], list)
const square = x =>
x * x
const gt = x => y =>
y > x
map (square, filter (gt (2), [ 1, 2, 3, 4 ]))
// => [ 9, 16 ]
使用我们自己的map,我们可以重写depth,使其更接近我们的原始形式...
const depth = ({ children = [] }) =>
isEmpty (children)
? 0
: 1 + foldL ( max
, 0
, map (depth, children)
)
但我希望我们停下来想想为什么这实际上比直接使用mapReduce 的depth 更糟糕...
够了
让我们花点时间想想我们在map-filter 示例中做了什么。 filter 遍历我们的整个输入数组,为每个数字调用gt (2),产生[ 3, 4 ] 的中间结果。 然后 map 调用 squarefor 中间结果中的数字,产生最终值 [ 9, 16 ]。数据变大了,我们不希望看到这样的代码:
myBigData.map(f).map(g).filter(h).map(i).map(j).reduce(k, base)
mapReduce 拥有一种会腐蚀其旁观者的力量。你以为我是心甘情愿写这个回答,其实我只是mapReduce的俘虏!这种结构是一些社区称之为transducers的东西的核心——恰好是a subject I've written about here on SO。我们开发了一种 折叠的折叠 直觉——就像魔术一样,用尽的多个循环折叠成一个折叠。如果您对此主题感兴趣,我鼓励您进一步阅读!