任何限制都是你自己的
其他人指出你在类型上犯了一个错误。您的每个函数都需要[k,v]
输入,但它们都不会输出这种形式 - 在这种情况下都不会compose(f,g)
或compose(g,f)
不会工作
无论如何,传感器是通用的,不需要知道它们处理的数据类型
const flip = ([ key, value ]) =>
[ value, key ]
const double = ([ key, value ]) =>
[ key, value * 2 ]
const pairToObject = ([ key, value ]) =>
({ [key]: value })
const entriesToObject = (iterable) =>
Transducer ()
.log ('begin:')
.map (double)
.log ('double:')
.map (flip)
.log ('flip:')
.map (pairToObject)
.log ('obj:')
.reduce (Object.assign, {}, Object.entries (iterable))
console.log (entriesToObject ({one: 1, two: 2}))
// begin: [ 'one', 1 ]
// double: [ 'one', 2 ]
// flip: [ 2, 'one' ]
// obj: { 2: 'one' }
// begin: [ 'two', 2 ]
// double: [ 'two', 4 ]
// flip: [ 4, 'two' ]
// obj: { 4: 'two' }
// => { 2: 'one', 4: 'two' }
当然,我们有标准的无聊数字数组,也有可能返回一个无聊的数字数组
const main = nums =>
Transducer ()
.log ('begin:')
.filter (x => x > 2)
.log ('greater than 2:')
.map (x => x * x)
.log ('square:')
.filter (x => x < 30)
.log ('less than 30:')
.reduce ((acc, x) => [...acc, x], [], nums)
console.log (main ([ 1, 2, 3, 4, 5, 6, 7 ]))
// begin: 1
// begin: 2
// begin: 3
// greater than 2: 3
// square: 9
// less than 30: 9
// begin: 4
// greater than 2: 4
// square: 16
// less than 30: 16
// begin: 5
// greater than 2: 5
// square: 25
// less than 30: 25
// begin: 6
// greater than 2: 6
// square: 36
// begin: 7
// greater than 2: 7
// square: 49
// [ 9, 16, 25 ]
更有趣的是,我们可以输入一个对象数组并返回一个集合
const main2 = (people = []) =>
Transducer ()
.log ('begin:')
.filter (p => p.age > 13)
.log ('age over 13:')
.map (p => p.name)
.log ('name:')
.filter (name => name.length > 3)
.log ('name is long enough:')
.reduce ((acc, x) => acc.add (x), new Set, people)
const data =
[ { name: "alice", age: 55 }
, { name: "bob", age: 16 }
, { name: "alice", age: 12 }
, { name: "margaret", age: 66 }
, { name: "alice", age: 91 }
]
console.log (main2 (data))
// begin: { name: 'alice', age: 55 }
// age over 13: { name: 'alice', age: 55 }
// name: alice
// name is long enough: alice
// begin: { name: 'bob', age: 16 }
// age over 13: { name: 'bob', age: 16 }
// name: bob
// begin: { name: 'alice', age: 12 }
// begin: { name: 'margaret', age: 66 }
// age over 13: { name: 'margaret', age: 66 }
// name: margaret
// name is long enough: margaret
// begin: { name: 'alice', age: 91 }
// age over 13: { name: 'alice', age: 91 }
// name: alice
// name is long enough: alice
// => Set { 'alice', 'margaret' }
看?我们可以执行您想要的任何类型的转换。你只需要一个Transducer
符合要求的
const identity = x =>
x
const Transducer = (t = identity) => ({
map: (f = identity) =>
Transducer (k =>
t ((acc, x) => k (acc, f (x))))
, filter: (f = identity) =>
Transducer (k =>
t ((acc, x) => f (x) ? k (acc, x) : acc))
, tap: (f = () => undefined) =>
Transducer (k =>
t ((acc, x) => (f (x), k (acc, x))))
, log: (s = "") =>
Transducer (t) .tap (x => console.log (s, x))
, reduce: (f = (a,b) => a, acc = null, xs = []) =>
xs.reduce (t (f), acc)
})
完整的程序演示 -.log
添加只是为了让您可以看到以正确的顺序发生的事情
const identity = x =>
x
const flip = ([ key, value ]) =>
[ value, key ]
const double = ([ key, value ]) =>
[ key, value * 2 ]
const pairToObject = ([ key, value ]) =>
({ [key]: value })
const Transducer = (t = identity) => ({
map: (f = identity) =>
Transducer (k =>
t ((acc, x) => k (acc, f (x))))
, filter: (f = identity) =>
Transducer (k =>
t ((acc, x) => f (x) ? k (acc, x) : acc))
, tap: (f = () => undefined) =>
Transducer (k =>
t ((acc, x) => (f (x), k (acc, x))))
, log: (s = "") =>
Transducer (t) .tap (x => console.log (s, x))
, reduce: (f = (a,b) => a, acc = null, xs = []) =>
xs.reduce (t (f), acc)
})
const entriesToObject = (iterable) =>
Transducer ()
.log ('begin:')
.map (double)
.log ('double:')
.map (flip)
.log ('flip:')
.map (pairToObject)
.log ('obj:')
.reduce (Object.assign, {}, Object.entries (iterable))
console.log (entriesToObject ({one: 1, two: 2}))
// begin: [ 'one', 1 ]
// double: [ 'one', 2 ]
// flip: [ 2, 'one' ]
// obj: { 2: 'one' }
// begin: [ 'two', 2 ]
// double: [ 'two', 4 ]
// flip: [ 4, 'two' ]
// obj: { 4: 'two' }
// => { 2: 'one', 4: 'two' }
函数式编程与函数式程序
JavaScript 不包含类似 的功能实用程序map
,filter
也不包含reduce
其他可迭代对象(例如 Generator、Map 或 Set)。在编写启用函数式编程的函数时,我们可以通过多种方式来实现 - 考虑不同的实现reduce
// possible implementation 1
const reduce = (f = (a,b) => a, acc = null, xs = []) =>
xs.reduce (f, acc)
// possible implementation 2
const reduce = (f = (a,b) => a, acc = null, [ x = Empty, ...xs ]) =>
isEmpty (x)
? acc
: reduce (f, f (acc, x) xs)
// possible implementation 3
const reduce = (f = (a,b) => a, acc = null, xs = []) =>
{
for (const x of xs)
acc = f (acc, x)
return acc
}
上面的每个实现都reduce
支持函数式编程;然而,只有一个实现本身就是一个功能程序
这只是 native 的包装Array.prototype.reduce
。它具有相同的缺点,Array.prototype.reduce
因为它仅适用于数组。在这里,我们很高兴现在可以使用普通函数编写 reduce 表达式,并且创建包装器很容易。但是,如果我们调用reduce (add, 0, new Set ([ 1, 2, 3 ]))
,它会失败,因为集合没有reduce
方法,这让我们感到难过。
这适用于现在的任何可迭代对象,但递归定义意味着如果堆栈非常大,它将溢出堆栈xs
- 至少在 JavaScript 解释器添加对尾调用消除的支持之前。在这里,我们对reduce
.
这适用于任何迭代,就像 #2 一样,但是我们必须用优雅的递归表达式换取for
确保堆栈安全的命令式循环。丑陋的细节让我们感到难过,reduce
但它让我们在程序中使用它的任何地方都感到高兴。
为什么这很重要?嗯,在Transducer
我分享的,reduce
我包含的方法是:
const Transducer (t = identity) =>
({ ...
, reduce: (f = (a,b) => a, acc = null, xs = []) =>
xs.reduce (t (f), acc)
})
这个特定的实现最接近我们reduce
上面的#1——它是一个快速而肮脏的包装器Array.prototype.reduce
。当然我们Transducer
可以对包含任何类型值的数组执行转换,但这意味着我们的 Transducer 只能接受数组作为输入。我们用灵活性换取了更容易的实施。
我们可以把它写成更接近风格#2,但是我们继承了堆栈漏洞,无论我们在大数据集上使用我们的转换器模块 - 这是转换器首先要表现出色的地方。更接近 #3 的实现本身不是函数式程序,但它支持函数式编程——
结果是一个必须利用 JavaScript 的一些命令式风格的模块,以使用户能够以无负担的方式编写函数式风格的程序
const Transducer (t = identity) =>
({ ...
, reduce: (f = (a,b) => a, acc = null, xs = []) =>
{
const reducer = t (f)
for (const x of xs)
acc = reducer (acc, x)
return acc
}
})
这里的想法是您可以编写自己的Transducer
模块并发明任何其他数据类型和实用程序来支持它。熟悉权衡使您能够选择最适合您的程序的任何东西。
围绕本节中提出的“问题”有很多方法。那么,如果我们在程序的各个部分都必须不断地恢复为命令式风格,那么如何才能真正用 JavaScript 编写函数式程序呢?没有灵丹妙药的答案,但我花了相当多的时间探索各种解决方案。如果你对这篇文章如此深入并且感兴趣,我在这里分享一些工作
可能性 #4
是的,您可以利用Array.from
它将任何可迭代对象转换为数组,这允许我们直接插入Array.prototype.reduce
. 现在的转换器可以接受任何可迭代的输入、功能风格和简单的实现——
这种方法的一个缺点是它创建了一个中间值数组(浪费内存),而不是在值从可迭代对象中出来时一次处理一个。请注意,即使解决方案#2 也有不小的缺点
const Transducer (t = identity) =>
({ ...
, reduce: (f = (a,b) => a, acc = null, xs = []) =>
Array.from (xs)
.reduce (t (f), acc)
})