如何通过传感器组合对象的变换

时间:2018-03-24 01:00:03

标签: typescript functional-programming function-composition transducer

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等工具。

2 个答案:

答案 0 :(得分:2)

您有任何限制

其他人指出你在使用这些类型时犯了错误。您的每个函数都需要[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等功能实用程序,用于其他迭代项,如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的每个实现都可以实现编程;但是,只有一个实现本身就是一个功能程序

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

  2. 这适用于任何可迭代的,但递归定义意味着如果xs非常大,它将溢出堆栈 - 至少在JavaScript解释器添加对尾部调用消除的支持之前。在这里,我们对reduce的代表性感到高兴,但无论我们在哪里使用它,我们的程序都让我们为它的致命弱点而悲伤

  3. 这适用于任何迭代,就像#2一样,但是我们必须交换优雅的递归表达式,以确保堆栈安全的命令式for循环。丑陋的细节让我们为reduce感到难过,但无论我们在程序中使用它,它都会让我们感到高兴。

  4. 为什么这很重要?好吧,在我分享的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的实现本身不是一个功能程序,但它启用函数式编程 -

    结果是一个模块必然利用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编写函数式程序呢?没有银弹答案,但我花了相当多的时间探索各种解决方案。如果你对这篇帖子很感兴趣并感兴趣,我会分享一些工作here

    可能性#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)
      })
    

答案 1 :(得分:1)

首先感谢您参加课程。 您在编写时遇到问题,因为我们在预期的输入和输出之间存在冲突的数据类型。

在组合flip和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]数组。这样,执行objReducer的{​​{1}}将正常工作,因为它期望将对象作为值。 这就是我在Object.assign添加的原因。

如果entriesToObject已更新为处理objReducer数组和对象 作为值,您可以继续从最后一步返回[k,v]数组,这是一个更好的方法

您可以在此处看到如何重写objReducer的示例: https://github.com/jlongster/transducers.js/blob/master/transducers.js#L766

对于生产用途,如果您使用该传感器库,您可以将输入和输出视为[k,v]数组,这是一种更好的方法。为了您自己的学习,您可以尝试根据该链接修改[k,v],然后您应该可以从上面的合成中删除objReducer

希望有所帮助!