如何通过传感器功能组合对象的变换
How to functional compose transforms of objects via transducers
Live code example
我正在尝试通过 egghead 学习换能器,我想我明白了,直到我们尝试组合对象转换。我在下面有一个不起作用的例子
const flip = map(([k,v]) => ({[v]: k}));
const double = map(([k,v]) => ({[k]: v + v}));
seq(flip, {one: 1, two: 2}); /*?*/ {1: 'one', 2: 'two'}
seq(double, {one: 1, two: 2}); /*?*/ {'one': 2, 'two: 4}
但是如果我写它失败了:
seq(compose(flip, double), {one: 1, two: 2}); /*?*/ {undefined: NaN}
seq(compose(double, flip), {one: 1, two: 2}); /*?*/ {undefined: undefined}
如何使用具有 fp 组合的传感器处理对象?
有很多样板文件,所以我真的建议查看实时代码示例以查看 compose、seq 等实用程序。
首先感谢您完成本课程。
您在编写时遇到问题,因为我们在预期的输入和输出之间遇到了冲突的数据类型。
当组合 flip 和 double 时,seq
帮助程序调用 transduce
帮助程序函数,该函数会将您的输入对象转换为 [k,v]
条目的数组,以便它可以遍历它.它还使用 objectReducer
帮助程序调用您的组合转换以用作内部减速器,它只是执行 Object.assign
以继续积累。
然后它遍历 [k,v]
条目,将它们传递给您的复合化简器,但由您来确保保持转换之间的数据类型兼容。
在您的示例中,double
将获得 flip
的 return 值,但 double
需要一个 [k,v]
数组,并翻转 return是一个对象。
所以你必须这样做:
const entriesToObject = map(([k,v]) => {
return {[k]:v};
});
const flipAndDouble = compose(
map(([k,v]) => {
return [k,v+v];
}),
map(([k,v]) => {
return [v,k];
}),
entriesToObject,
);
//{ '2': 'one', '4': 'two', '6': 'three' }
这有点令人困惑,因为您必须确保最后一步 return 是一个对象而不是 [k,v]
数组。这样执行 Object.assign
的 objReducer
将正常工作,因为它需要一个对象作为值。
这就是为什么我在上面添加 entriesToObject
。
如果更新 objReducer
以处理 [k,v]
数组和对象
作为值,那么您也可以从最后一步保留 returning [k,v]
数组,这是一个更好的方法
您可以在此处查看如何重写 objReducer 的示例:
https://github.com/jlongster/transducers.js/blob/master/transducers.js#L766
对于生产用途,如果您使用那个传感器库,您可以继续将输入和输出视为 [k,v] 数组,这是一种更好的方法。为了您自己的学习,您可以尝试根据 link 修改 objReducer
,然后您应该能够从上面的组合中删除 entriesToObject
。
希望对您有所帮助!
任何限制都是你自己的
其他人指出您的类型有误。您的每个函数都需要 [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' }
当然我们有标准的无聊数字数组,return也有可能是无聊的数字数组
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 ]
更有趣的是,我们可以输入一个对象数组和return一个集合
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' }
函数式编程 vs 函数式程序
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
的每个实现都启用功能性 编程 ;但是,只有一个实现本身是功能性的 程序
这只是本机 Array.prototype.reduce
的包装器。它具有与 Array.prototype.reduce
相同的缺点,因为它仅适用于数组。在这里我们很高兴我们现在可以使用普通函数编写 reduce 表达式并且创建包装器很容易。但是,如果我们调用 reduce (add, 0, new Set ([ 1, 2, 3 ]))
,它会失败,因为集合没有 reduce
方法,这让我们很难过。
这现在适用于任何可迭代对象,但递归定义意味着如果 xs
非常大,它将溢出堆栈 - 至少在 JavaScript 解释器添加对尾调用消除。在这里,我们很高兴我们对 reduce
的表示,但无论我们在什么地方使用它,我们的程序都对它的致命弱点感到难过
这与#2 一样适用于任何可迭代对象,但是我们必须将优雅的递归表达式换成确保堆栈安全的 imperative-style 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,但是无论我们在大数据集上使用我们的传感器模块,我们都会继承堆栈漏洞——这就是传感器的目的是excel 首先。接近 #3 的实现本身不是函数式程序,但它 启用 函数式编程 —
结果是 必须 使用 JavaScript 的一些 imperative-style 以使用户能够编写 functional-style 的模块以无负担的方式进行程序
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
模块并发明任何其他数据类型和实用程序来支持它。熟悉 trade-offs 使 你 能够选择最适合 你的 程序的东西。
围绕本节中介绍的 "problem" 有很多方法。那么,如果我们不得不在程序的各个部分不断地恢复到命令式风格,那么如何才能真正在 JavaScript 中编写函数式程序呢?没有灵丹妙药,但我花了相当多的时间探索各种解决方案。如果您对 post 如此深入并且感兴趣,我会分享其中的一些工作
可能性 #4
是的,您可以利用 Array.from
将任何可迭代对象转换为数组,这样我们就可以直接插入 Array.prototype.reduce
。现在接受任何可迭代输入的转换器,函数式风格,and 一个简单的实现 —
这种方法的一个缺点是它创建了一个中间值数组(浪费内存)而不是处理来自可迭代对象的值 one-at-a-time。请注意,即使是解决方案 #2 也有 non-trivial 缺点
const Transducer (t = identity) =>
({ ...
, reduce: (f = (a,b) => a, acc = null, xs = []) =>
Array.from (xs)
.reduce (t (f), acc)
})
Live code example
我正在尝试通过 egghead 学习换能器,我想我明白了,直到我们尝试组合对象转换。我在下面有一个不起作用的例子
const flip = map(([k,v]) => ({[v]: k}));
const double = map(([k,v]) => ({[k]: v + v}));
seq(flip, {one: 1, two: 2}); /*?*/ {1: 'one', 2: 'two'}
seq(double, {one: 1, two: 2}); /*?*/ {'one': 2, 'two: 4}
但是如果我写它失败了:
seq(compose(flip, double), {one: 1, two: 2}); /*?*/ {undefined: NaN}
seq(compose(double, flip), {one: 1, two: 2}); /*?*/ {undefined: undefined}
如何使用具有 fp 组合的传感器处理对象?
有很多样板文件,所以我真的建议查看实时代码示例以查看 compose、seq 等实用程序。
首先感谢您完成本课程。 您在编写时遇到问题,因为我们在预期的输入和输出之间遇到了冲突的数据类型。
当组合 flip 和 double 时,seq
帮助程序调用 transduce
帮助程序函数,该函数会将您的输入对象转换为 [k,v]
条目的数组,以便它可以遍历它.它还使用 objectReducer
帮助程序调用您的组合转换以用作内部减速器,它只是执行 Object.assign
以继续积累。
然后它遍历 [k,v]
条目,将它们传递给您的复合化简器,但由您来确保保持转换之间的数据类型兼容。
在您的示例中,double
将获得 flip
的 return 值,但 double
需要一个 [k,v]
数组,并翻转 return是一个对象。
所以你必须这样做:
const entriesToObject = map(([k,v]) => {
return {[k]:v};
});
const flipAndDouble = compose(
map(([k,v]) => {
return [k,v+v];
}),
map(([k,v]) => {
return [v,k];
}),
entriesToObject,
);
//{ '2': 'one', '4': 'two', '6': 'three' }
这有点令人困惑,因为您必须确保最后一步 return 是一个对象而不是 [k,v]
数组。这样执行 Object.assign
的 objReducer
将正常工作,因为它需要一个对象作为值。
这就是为什么我在上面添加 entriesToObject
。
如果更新 objReducer
以处理 [k,v]
数组和对象
作为值,那么您也可以从最后一步保留 returning [k,v]
数组,这是一个更好的方法
您可以在此处查看如何重写 objReducer 的示例: https://github.com/jlongster/transducers.js/blob/master/transducers.js#L766
对于生产用途,如果您使用那个传感器库,您可以继续将输入和输出视为 [k,v] 数组,这是一种更好的方法。为了您自己的学习,您可以尝试根据 link 修改 objReducer
,然后您应该能够从上面的组合中删除 entriesToObject
。
希望对您有所帮助!
任何限制都是你自己的
其他人指出您的类型有误。您的每个函数都需要 [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' }
当然我们有标准的无聊数字数组,return也有可能是无聊的数字数组
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 ]
更有趣的是,我们可以输入一个对象数组和return一个集合
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' }
函数式编程 vs 函数式程序
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
的每个实现都启用功能性 编程 ;但是,只有一个实现本身是功能性的 程序
这只是本机
Array.prototype.reduce
的包装器。它具有与Array.prototype.reduce
相同的缺点,因为它仅适用于数组。在这里我们很高兴我们现在可以使用普通函数编写 reduce 表达式并且创建包装器很容易。但是,如果我们调用reduce (add, 0, new Set ([ 1, 2, 3 ]))
,它会失败,因为集合没有reduce
方法,这让我们很难过。这现在适用于任何可迭代对象,但递归定义意味着如果
xs
非常大,它将溢出堆栈 - 至少在 JavaScript 解释器添加对尾调用消除。在这里,我们很高兴我们对reduce
的表示,但无论我们在什么地方使用它,我们的程序都对它的致命弱点感到难过这与#2 一样适用于任何可迭代对象,但是我们必须将优雅的递归表达式换成确保堆栈安全的 imperative-style
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,但是无论我们在大数据集上使用我们的传感器模块,我们都会继承堆栈漏洞——这就是传感器的目的是excel 首先。接近 #3 的实现本身不是函数式程序,但它 启用 函数式编程 —
结果是 必须 使用 JavaScript 的一些 imperative-style 以使用户能够编写 functional-style 的模块以无负担的方式进行程序
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
模块并发明任何其他数据类型和实用程序来支持它。熟悉 trade-offs 使 你 能够选择最适合 你的 程序的东西。
围绕本节中介绍的 "problem" 有很多方法。那么,如果我们不得不在程序的各个部分不断地恢复到命令式风格,那么如何才能真正在 JavaScript 中编写函数式程序呢?没有灵丹妙药,但我花了相当多的时间探索各种解决方案。如果您对 post 如此深入并且感兴趣,我会分享其中的一些工作
可能性 #4
是的,您可以利用 Array.from
将任何可迭代对象转换为数组,这样我们就可以直接插入 Array.prototype.reduce
。现在接受任何可迭代输入的转换器,函数式风格,and 一个简单的实现 —
这种方法的一个缺点是它创建了一个中间值数组(浪费内存)而不是处理来自可迭代对象的值 one-at-a-time。请注意,即使是解决方案 #2 也有 non-trivial 缺点
const Transducer (t = identity) =>
({ ...
, reduce: (f = (a,b) => a, acc = null, xs = []) =>
Array.from (xs)
.reduce (t (f), acc)
})