【问题标题】:How to functional compose transforms of objects via transducers如何通过传感器功能组合对象的变换
【发布时间】:2018-03-24 01:00:03
【问题描述】:

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 等实用程序。

【问题讨论】:

  • 请注意,失败的第一件事根本不应该工作,传感器被向后馈送到compose。我还认为传感器只在接收到它们的缩减阶跃函数之前组合:const map = xform => reducer => (accum, input) => reducer(accum, xform(input));,它只在第一个返回的函数中组合。
  • 我知道 compose 从右到左工作,但是 2 +2 或 'two'+'two' 编译就好了。如果你查看 seq 或 sequence 的实时代码,你会看到 reducer 是如何处理的。
  • 对于换能器,您可以按照与正常组合相反的顺序喂它们。如果你想加倍然后翻转它将是 compose(double, flip) 与传感器与普通函数组合 compose(flip, double)。
  • 至于您的代码,我会先尝试使用数字数组。例如,如果您采用我上面对map 的定义并执行const double = map(x => x * 2); const plusOne = map(x => x + 1); const append = (acc, inp) => (Array.isArray(acc) ? acc : [acc]).concat([inp]);,然后执行[1,2].reduce(compose(plusOne, double)(append), []),您将得到预期的[4,6]。
  • @JaredSmith seq/tranduce 100% 适用于数字数组。只是不是对象。

标签: typescript functional-programming function-composition transducer


【解决方案1】:

任何限制都是您自己的

其他人指出您在类型上犯了错误。您的每个函数都需要 [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 的每个实现都支持函数式编程;然而,只有一个实现本身就是一个函数式程序

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

  2. 1234563在这里,我们对 reduce 的表示感到高兴,但无论我们在哪里使用它,我们的程序都对它的致命弱点感到难过
  3. 这适用于任何迭代,就像 #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 编写函数式程序呢?没有灵丹妙药的答案,但我花了相当多的时间探索各种解决方案。如果你对这篇文章有这么深的兴趣并感兴趣,我会分享其中的一些工作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)
  })

【讨论】:

  • 这个答案说的太好了!非常感谢你,娜奥米克!有很多东西要拆开她,但它很棒!非常感谢。
【解决方案2】:

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

在组合翻转和双精度时,seq 助手调用 transduce 助手函数,它将您的输入对象转换为 [k,v] 条目的数组,以便它可以遍历它。它还使用 objectReducer 帮助器调用您的组合变换以用作内部减速器,它只是执行 Object.assign 以继续建立累积。

然后它会遍历[k,v] 条目,将它们传递给您的组合reducer,但您需要确保在转换之间保持数据类型兼容。

在您的示例中,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.assign 的objReducer 将正常工作,因为它需要一个对象作为值。 这就是我在上面添加entriesToObject 的原因。

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

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

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

希望有帮助!

【讨论】:

    猜你喜欢
    • 2020-01-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-20
    • 1970-01-01
    • 1970-01-01
    • 2018-06-01
    • 1970-01-01
    相关资源
    最近更新 更多