2

实时代码示例

我正在尝试通过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 等实用程序。

4

2 回答 2

3

任何限制都是你自己的

其他人指出你在类型上犯了一个错误。您的每个函数都需要[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 不包含类似 的功能实用程序mapfilter也不包含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. 这只是 native 的包装Array.prototype.reduce。它具有相同的缺点,Array.prototype.reduce因为它仅适用于数组。在这里,我们很高兴现在可以使用普通函数编写 reduce 表达式,并且创建包装器很容易。但是,如果我们调用reduce (add, 0, new Set ([ 1, 2, 3 ])),它会失败,因为集合没有reduce方法,这让我们感到难过。

  2. 这适用于现在的任何可迭代对象,但递归定义意味着如果堆栈非常大,它将溢出堆栈xs- 至少在 JavaScript 解释器添加对尾调用消除的支持之前。在这里,我们对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 编写函数式程序呢?没有灵丹妙药的答案,但我花了相当多的时间探索各种解决方案。如果你对这篇文章如此深入并且感兴趣,我在这里分享一些工作

可能性 #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)
  })
于 2018-03-25T09:00:16.917 回答
1

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

在组合翻转和双精度时,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执行的操作将正常工作。Object.assign这就是我在entriesToObject上面添加的原因。

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

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

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

希望有帮助!

于 2018-03-25T03:58:09.933 回答