【问题标题】:Functional Programming: How to convert an impure function to a pure function when the input needs to be mutated函数式编程:当输入需要改变时,如何将不纯函数转换为纯函数
【发布时间】:2020-07-18 19:35:42
【问题描述】:

如何创建一个纯函数来更新在另一个函数中初始化的对象 类似:

parentFunction = (inputs: object[], condtionList: string[]) => {

  const newObject = {f1: val1[], f2: val2[], f3: val3[]...}
  inputs.forEach(input => {
    if(condition1){
      updateNewObject(condtion1, newObject, input, conditionList)
    }
    .
    .
    . 
  }
  return newObject
}

下面的函数是不纯的,因为它正在更新 newObject(改变输入)我如何将它转换为纯函数?

updateNewObject(condition, newObject, input, conditionList) {
  const i = conditionList.indexOf(input.condition)
  if(i === 0){
    newObject.f1.push(input)
  }
  else if(i === 1) {
    newObject.f2.push(input)
  }
  .
  .
  .
}

上面没有返回值。它将 newObject 作为输入,并根据一些条件将值推送到 newObject 的属性。反正有没有使上述功能变得纯粹?还是我必须重新考虑如何更新 newObject?

【问题讨论】:

  • JS Object/Array 类型不依赖于设计的可变性!只是不要改变输入。如果您关心效率,请使用不可变数据类型。
  • 你是说你不能改变parentFunction?

标签: javascript typescript functional-programming purely-functional


【解决方案1】:

函数式编程不仅关乎纯度,还关乎可重用性和关注点分离。编写一个大而复杂的函数很难,更难测试和维护它。遵循功能原则将帮助我们避免疼痛和不适。

让我们从隔离我们关心的行为开始。我们识别函数 push、update 和 pushKey -

const identity = x =>
  x

const push = (a = [], value) =>
  a.concat([ value ])

const update = (o = {}, key = "", t = identity) =>
  ({ ...o, [key]: t(o[key]) })

const pushKey = (o = {}, key = "", value) =>
  update(o, key, a => push(a, value))

这使您可以轻松地执行基本的不可变转换 -

const d1 = { a: [1], b: [] }
const d2 = pushKey(d1, "a", 2)
const d3 = pushKey(d2, "b", 3)
const d4 = pushKey(d3, "c", 4)

console.log(d1) // { a: [1], b: [] }
console.log(d2) // { a: [1, 2], b: [] }
console.log(d3) // { a: [1, 2], b: [3] }
console.log(d4) // { a: [1, 2], b: [3], c: [4] }

展开下面的sn-p,在自己的浏览器中运行程序-

const identity = x =>
  x

const push = (a = [], value) =>
  a.concat([ value ])

const update = (o = {}, key = "", t = identity) =>
  ({ ...o, [key]: t(o[key]) })

const pushKey = (o = {}, key = "", value) =>
  update(o, key, a => push(a, value))

const d1 = { a: [1], b: [] }
const d2 = pushKey(d1, "a", 2)
const d3 = pushKey(d2, "b", 3)
const d4 = pushKey(d3, "c", 4)

console.log(JSON.stringify(d1)) // { a: [1], b: [] }
console.log(JSON.stringify(d2)) // { a: [1, 2], b: [] }
console.log(JSON.stringify(d3)) // { a: [1, 2], b: [3] }
console.log(JSON.stringify(d4)) // { a: [1, 2], b: [3], c: [4] }

这允许您将复杂的条件逻辑分离到它自己的函数中 -

const updateByCondition = (o = {}, conditions = [], ...) =>
{ if (...)
    return pushKey(o, "foo", someValue)
  else if (...)
    return pushKey(o, "bar", someValue) 
  else
    return pushKey(o, "default", someValue) 
}

这种方法的优点很多。 push、update 和 pushKey 都非常容易编写、测试和维护,并且很容易在我们程序的其他部分中重用。写updateByCondition 要容易得多,因为我们有更好的基本构建块。由于您尝试编码的任何复杂性,它仍然难以测试,但是由于关注点分离,它更容易维护。

【讨论】:

  • 也许展示了如何将这些应用到 OP 的特定对象+数组更新。
  • sn-p 有帮助,但使用 OP 的名称会显着提高可读性。
【解决方案2】:

通常,您会创建一个带有新数组的新对象,其中包含新条目:

if(i === 0){
  //          v−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−v−− creates new object
  newObject = {...newObject, f1: [...newObject.f1, input]}
  //          ^                  ^−−−−−−−−−−−−−−−−−−−−−−^−−− creates new array
}
else if(i === 1) {
  //          v−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−v−− creates new object
  newObject = {...newObject, f2: [...newObject.f2, input]}
  //          ^                  ^−−−−−−−−−−−−−−−−−−−−−−^−−− creates new array
}

然后在parentFunction:

    newObject = updateNewObject(condtion1, newObject, input, conditionList)
//  ^^^^^^^^^^^^−−−−−−−−−−−−−−−−−−−−−− updates the value being returned

或者更新可能是:

const name = i === 0 ? "f1" : (i === 1 ? "f2" : ""));
if (name) {
  newObject = {...newObject, [name]: [...newObject[name], input]}
}

...虽然嵌套条件有点笨拙。 :-)

【讨论】:

  • name = ["f1", "f2"][i] ?? "" :-)
【解决方案3】:

如果你希望updateNewObject 是纯的,让它创建一个克隆原始对象的新对象,对其进行变异,然后返回新对象。

updateNewObject(condition, oldObject, input, conditionList) {
  const newObject = {...oldObject};
  const i = conditionList.indexOf(input.condition)
  if(i === 0){
    newObject.f1 = [...newObject.f1, input];
  }
  else if(i === 1) {
    newObject.f2 = [...newObject.f2, input];
  }
  .
  .
  .

  return newObject;
}

注意newObject.f1 = [...newObject.f1, input]; 如何创建一个新数组 - 这确保我们不仅不会直接改变对象,而且不会改变它的任何字段(数组)而是创建新的。

然后调整parentFunction 使其使用每个返回的updateNewObject 调用的值:

parentFunction = (inputs: object[], condtionList: string[]) => {

  let newObject = {f1: val1[], f2: val2[], f3: val3[]...}
  inputs.forEach(input => {
    if(condition1){
      newObject = updateNewObject(condtion1, newObject, input, conditionList)
    }
    .
    .
    . 
  }
  return newObject
}

【讨论】:

  • @T.J.Crowder 不-检查行const newObject = {...oldObject};
  • 啊,是的,对不起。 :-) 但是,当您不更新对象时,我看不出有任何复制对象的理由(当i 既不是0 也不是1 时)。
【解决方案4】:

只需复制/映射数组即可获得一个新数组。不要突变同一个。

【讨论】:

  • 我不明白对这个答案的反对意见。这是正确的。真的很简洁,但它是正确的。
  • @T.J.Crowder 它发生了:D
猜你喜欢
  • 1970-01-01
  • 2020-06-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-05
相关资源
最近更新 更多