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

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.assignobjReducer 将正常工作,因为它需要一个对象作为值。 这就是为什么我在上面添加 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 不包括 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 一样适用于任何可迭代对象,但是我们必须将优雅的递归表达式换成确保堆栈安全的 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)
  })