【问题标题】:Idiomatic `obj.value = f(obj)` in Ramda?Ramda 中惯用的“obj.value = f(obj)”?
【发布时间】:2019-08-10 03:31:45
【问题描述】:

R.evolve 让我们用应用于该属性当前值的函数的结果替换对象属性:

R.evolve({ count: R.inc }, { count: 1 })
   == { count: 2 }

但我经常发现我想添加一个从输入对象的多个属性计算的属性:

assocFruitTotal({ appleCount: 5, orangeCount: 3 })
  == { appleCount: 5, orangeCount: 3, fruitCount: 8 }

我想出了自己的简单实用函数:

const assocDerived = R.curry(
   (name, f, obj) => ({
      ...obj,
      [name]: f(obj)
   });

...我经常使用它:

const sumFruit = R.pipe(
   R.props(['appleCount', 'orangeCount']),
   R.sum);
const assocFruitTotal = assocDerived('fruitCount', sumFruit);

但是我使用它的频率让我想知道为什么它不是 Ramda 原生,就像许多其他方便的功能一样。这使得 我想知道我是否错过了一个更好的实现结果的习惯用法 - 即通过添加基于其他属性组合的属性来构建对象中的细节。

我应该使用惯用的函数式编程结构吗?

【问题讨论】:

  • 更通用的函数式成语是comonads,用于解包值及其上下文。
  • 提出这个建议的 PR 会受到 Ramda 团队的欢迎。扩展evolve 或添加这样的新函数可能是有意义的。不能保证它会被接受,但它会得到公平的听证。
  • applySpeco[prop] = f(o) 完全相同,请记住返回的 f 可能需要强制为 unary

标签: functional-programming ramda.js


【解决方案1】:

我个人会这样做:

const fruitCount = applySpec({fruitCount: compose(sum, values)})

fruitCount({apple: 5, orange: 3})
//=> {"fruitCount": 8}

const withFruitCount = converge(mergeRight, [identity, fruitCount]);

withFruitCount({apple: 5, orange: 3});
//=> {"apple": 5, "fruitCount": 8, "orange": 3}

如果有非计数属性要从总和中排除,您可以使用pickBy

const pickCount = pickBy(flip(includes('Count')));

pickCount({appleCount: 5, orangeCount: 3, foo: 'bar'});
//=> {"appleCount": 5, "orangeCount": 3}

【讨论】:

  • 我想问题是这个成语是否足够普遍,以至于读者总是会立即理解它。当然,我也可以写我的assocDerived 类似assocDerived =(name, fn) => converge(mergeRight, [ identity, applySpec({[name]: fn})])
【解决方案2】:

让我们首先认识到obj.value = f(obj) 是一个可变赋值,因此一开始就不是一个函数式惯用语。这是工作中的命令式思维。

在大多数情况下,将计算值作为属性存储在对象上是错误的。如果appleCountorangeCount 发生变化,则没有任何东西可以强制fruitCount 的完整性。

fruitCount 应该是一个函数,而不是一个属性。

const fruitCount =
  pipe
    ( props ([ 'appleCount', 'orangeCount' ])
    , sum
    )

 fruitCount ({ appleCount: 1, orangeCount: 3 }) // 4
 fruitCount ({ appleCount: 5, orangeCount: 3 }) // 8

如果我不得不猜测,这是假数据和示例问题。在某些情况下,计算值确实有意义(记忆是首先想到的技术),但这些情况构成了例外,而不是规则。你说“我使用这个的绝对频率......”,所以我敢打赌你会在比你应该做的更多领域这样做。

正如你所指出的,Ramda 没有内置的,所以这应该进一步表明有更传统的方法可以解决这类问题。


面向对象的程序员会将其分配为计算属性 -

const FruitData = function (apples = 0, oranges = 0)
{ this.apples = apples
  this.oranges = oranges
}

Object.defineProperty
  ( FruitData.prototype
  , 'fruitCount'
  , { get () { return this.apples + this.oranges } }
  )
  
const f =
  new FruitData (3, 4)
  
console .log (f.fruitCount) // 7

在编写函数式风格时,我们将 OOP 概念留在了门外。开始思考功能,你的问题就会消失 -

const FruitData = (apples = 0, oranges = 0) =>
  ({ apples, oranges })

const appleCount = fd =>
  fd.apples

const orangeCount = fd =>
  fd.oranges

const fruitCount = fd =>
  appleCount (fd) + orangeCount (fd)

console .log (fruitCount (FruitData (10, 3))) // 13

【讨论】:

  • 虽然我对问题的标题有点不精确,但为了简洁起见,我认为从问题的主体中可以清楚地看出我不想改变一个对象。我的assocDerived,如R.assocR.evolve 返回输入的新克隆,并替换了请求的道具。你的fruitcount是很明显的东西,但是当进一步降低调用组合时没有用,你仍然需要输入的其他部分。 pipe( assocDerived('fruitCount', sumFruit), o => 'You have ${o.oranges} oranges, ${o.apples} apples for a total of ${fruitCount} fruit')
  • 您对无积分的痴迷是hurting you(fd => `You have ${orangeCount(fd)} oranges, ${appleCount(fd)} apples for a total of ${fruitCount(fd)}`) 是一个非常好的函数。
猜你喜欢
  • 1970-01-01
  • 2016-05-28
  • 2019-04-20
  • 2015-12-31
  • 1970-01-01
  • 2020-02-22
  • 2016-04-16
  • 1970-01-01
  • 2019-03-20
相关资源
最近更新 更多