在我们更好地了解之前
我真的很喜欢链...
我明白了,我会安抚你,但你会明白强制你的程序通过链接 API 是不自然的,而且在大多数情况下比它值得的麻烦更多。
const Transduce = (reducer = (acc, x) => (acc.push(x), acc)) => ({
map: map => Transduce(mapper(map)(reducer)),
filter: pred => Transduce(filterer(pred)(reducer)),
run: arr => arr.reduce(reducer, [])
});
我想我明白它为什么会发生,但我无法弄清楚如何在不改变我的函数的“接口”的情况下修复它。
问题确实出在您的Transduce
构造函数上。您的map
和filter
方法在换能器链的外部堆叠map
并pred
放置,而不是将它们嵌套在内部。
下面,我已经实现了您的Transduce
API,它以正确的顺序评估地图和过滤器。我还添加了一个log
方法,以便我们可以查看Transduce
行为
const Transduce = (f = k => k) => ({
map: g =>
Transduce(k =>
f ((acc, x) => k(acc, g(x)))),
filter: g =>
Transduce(k =>
f ((acc, x) => g(x) ? k(acc, x) : acc)),
log: s =>
Transduce(k =>
f ((acc, x) => (console.log(s, x), k(acc, x)))),
run: xs =>
xs.reduce(f((acc, x) => acc.concat(x)), [])
})
const foo = nums => {
return Transduce()
.log('greater than 2?')
.filter(x => x > 2)
.log('\tsquare:')
.map(x => x * x)
.log('\t\tless than 30?')
.filter(x => x < 30)
.log('\t\t\tpass')
.run(nums)
}
// keep square(n), forall n of nums
// where n > 2
// where square(n) < 30
console.log(foo([1,2,3,4,5,6,7]))
// => [ 9, 16, 25 ]
未开发的潜力
受到这个答案的启发......
在阅读我写的那个答案时,你忽略了Trans
它写在那里的一般质量。在这里,我们Transduce
只尝试使用数组,但实际上它可以使用任何具有空值 ( []
) 和concat
方法的类型。这两个属性构成了一个称为Monoids的类别,如果我们不利用转换器处理该类别中任何类型的能力,我们会对自己造成伤害。
上面,我们[]
在run
方法中硬编码了初始累加器,但这可能应该作为参数提供——就像我们做的一样iterable.reduce(reducer, initialAcc)
除此之外,两种实现本质上是等效的。最大的区别是Trans
链接答案中提供的实现Trans
本身是一个幺半群,但Transduce
这里不是。Trans
在concat
方法中巧妙地实现了换能器的组合,而Transduce
(上图)在每个方法中混合了组合。使其成为一个独异使我们能够合理化Trans
以同样的方式做所有其他类群,而不必把它理解为一些专门的链接接口,具有唯一,map
,filter
,和run
方法。
我建议从构建Trans
而不是创建自己的自定义 API
有你的蛋糕,也吃它
所以我们学到了统一接口的宝贵教训,我们明白这Trans
本质上很简单。但是,您仍然需要那个甜蜜的链接 API。好的好的...
我们将再实现Transduce
一次,但这次我们将使用Trans
幺半群来实现。在这里,Transduce
持有一个Trans
值而不是一个延续 ( Function
)。
其他一切都保持不变——foo
只需进行 1 次微小的更改,即可产生相同的输出。
// generic transducers
const mapper = f =>
Trans(k => (acc, x) => k(acc, f(x)))
const filterer = f =>
Trans(k => (acc, x) => f(x) ? k(acc, x) : acc)
const logger = label =>
Trans(k => (acc, x) => (console.log(label, x), k(acc, x)))
// magic chaining api made with Trans monoid
const Transduce = (t = Trans.empty()) => ({
map: f =>
Transduce(t.concat(mapper(f))),
filter: f =>
Transduce(t.concat(filterer(f))),
log: s =>
Transduce(t.concat(logger(s))),
run: (m, xs) =>
transduce(t, m, xs)
})
// when we run, we must specify the type to transduce
// .run(Array, nums)
// instead of
// .run(nums)
展开这个代码片段,看看最终的实现-当然,你可以跳过定义一个独立的mapper
,filterer
和logger
,而是直接在定义这些Transduce
。我认为这读起来更好。
// Trans monoid
const Trans = f => ({
runTrans: f,
concat: ({runTrans: g}) =>
Trans(k => f(g(k)))
})
Trans.empty = () =>
Trans(k => k)
const transduce = (t, m, xs) =>
xs.reduce(t.runTrans((acc, x) => acc.concat(x)), m.empty())
// complete Array monoid implementation
Array.empty = () => []
// generic transducers
const mapper = f =>
Trans(k => (acc, x) => k(acc, f(x)))
const filterer = f =>
Trans(k => (acc, x) => f(x) ? k(acc, x) : acc)
const logger = label =>
Trans(k => (acc, x) => (console.log(label, x), k(acc, x)))
// now implemented with Trans monoid
const Transduce = (t = Trans.empty()) => ({
map: f =>
Transduce(t.concat(mapper(f))),
filter: f =>
Transduce(t.concat(filterer(f))),
log: s =>
Transduce(t.concat(logger(s))),
run: (m, xs) =>
transduce(t, m, xs)
})
// this stays exactly the same
const foo = nums => {
return Transduce()
.log('greater than 2?')
.filter(x => x > 2)
.log('\tsquare:')
.map(x => x * x)
.log('\t\tless than 30?')
.filter(x => x < 30)
.log('\t\t\tpass')
.run(Array, nums)
}
// output is exactly the same
console.log(foo([1,2,3,4,5,6,7]))
// => [ 9, 16, 25 ]
包起来
所以我们从一堆 lambda 开始,然后使用幺半群使事情变得更简单。的Trans
半群,所述半群接口是已知的提供不同的优点和通用的实现是非常简单的。但是我们很顽固,或者我们有一些目标要实现,而这些目标不是由我们设定的——我们决定构建神奇的Transduce
链式 API,但我们使用坚如磐石的Trans
幺半群来实现,这为我们提供了所有的力量,Trans
但也很好地保持了复杂性划分的。
点链恋物癖匿名
这是我最近写的关于方法链的其他几个答案