如何通过换能器构成对象的变换



实时代码示例

我正在尝试通过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等的效果。

任何限制都是您自己的

其他人指出,您正在对这些类型犯错。您的每个功能都期望[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不包括用于生成器,映射或设置的其他迭代物的功能实用程序,例如mapfilterreduce。编写启用函数编程的函数时,我们可以以多种方式进行操作 - 考虑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的每个实现都可以启用功能编程;但是,只有一个实现本身就是功能程序

  1. 这只是本机Array.prototype.reduce周围的包装器。它具有与Array.prototype.reduce相同的缺点,因为它仅适用于数组。在这里,我们很高兴现在可以使用普通功能编写减少表达式,并且创建包装器很容易。但是,如果我们调用reduce (add, 0, new Set ([ 1, 2, 3 ])),它会失败,因为设置没有reduce方法,这会让我们感到难过。

  2. 现在可以在任何可触觉的情况下起作用,但是递归定义意味着如果xs显着大,它将溢出堆栈 - 至少在JavaScript口译员添加了对尾声消除的支持之前。在这里,我们对reduce的代表感到满意,但是无论我们在哪里使用它,我们的程序都为其致命的幕后脚跟

  3. 感到难过
  4. 这在任何可觉得的效果上都可以,但是我们必须将优雅的递归表达方式换成命令风格的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可以在包含任何类型的值的数组上执行转换,但这意味着我们的传感器只能接受数组作为输入。我们进行了灵活性以更轻松地实施。

我们可以将其更接近样式#2写,但是随后我们在大数据集上使用换能器模块的任何位置都继承了堆栈漏洞 - 这是传感器首先要脱颖而出的地方。接近#3的实现本身不是功能程序,而是启用功能编程&mdash;

结果是一个模块, 使用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
     }
  })

这里的想法是 you 可以编写自己的Transducer模块并发明任何其他数据类型和实用程序来支持它。熟悉权衡的熟悉 you 可以选择最适合您的您的程序。

本节中介绍的"问题"有很多方法。那么,如果我们不得不在程序的各个部分中不得不恢复命令风格,该如何真正在JavaScript中编写功能程序呢?没有银色子弹的答案,但是我花了很多时间探索各种解决方案。如果您在帖子中如此深入并有兴趣,我在这里分享其中的一些工作

可能性#4

是的,您可以利用将任何可触及的数组转换为数组的Array.from,这使我们可以直接插入Array.prototype.reduce。现在,可以接受任何可触及的输入,功能样式的传感器,一个简单的实现&mdash;

这种方法的缺点是,它创建了一个中间值(浪费的内存(,而不是在一个时间中从一个时间出来时处理值。注意,即使解决方案#2共享非平凡的缺点

const Transducer (t = identity) =>
  ({ ...
   , reduce: (f = (a,b) => a, acc = null, xs = []) =>
       Array.from (xs)
         .reduce (t (f), acc)
  })

首先要感谢您参加课程。您在编写困难,因为我们在预期输入和输出之间存在冲突数据类型。

编写翻转和double时,seq助手调用transduce辅助功能,该功能将您的输入对象转换为[k,v]条目的数组,以便可以通过它迭代。它还称您使用objectReducer助手使用的组合变换为内部还原器,该变形器仅执行Object.assign来继续积累。

然后,它通过[k,v]条目迭代,将它们传递到您组成的还原器,但要确保您将数据类型保持在转换之间的兼容。

在您的示例中,double将获得flip的返回值,但是double期望[k,v]数组,翻转返回对象。

所以您必须做这样的事情:

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' }​​​​​

这有点令人困惑,因为您必须确保最后一步返回对象而不是[k,v]数组。因此,执行Object.assignobjReducer由于期望对象为值,因此可以正常工作。这就是为什么我在上面的entriesToObject中添加。

如果更新了objReducer以处理[k,v]数组以及对象随着值,您也可以继续从最后一步返回[k,v]数组,这是一个更好的方法

您可以看到一个示例,说明如何在此处重写ObjreDucer:https://github.com/jlongster/transducers.js/blob/master/transducers.js#l766

用于生产使用,如果您使用该换能器库,则可以继续将输入和输出视为[K,V]数组,这是一种更好的方法。对于自己的学习,您可以尝试根据该链接修改objReducer,然后您应该能够从上面的组合中删除entriesToObject

希望有帮助!

最新更新