函数式编程:当需要改变输入时如何将不纯函数转换为纯函数

Functional Programming: How to convert an impure function to a pure function when the input needs to be mutated

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

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)
  }
  .
  .
  .
}

以上没有return值。它将 newObject 作为输入,并根据某些条件将值推送到 newObject 的属性。有没有办法让上面的功能变得纯粹?还是我必须重新考虑如何更新 newObject?

只需 copy/map 数组即可获得一个新数组。不要变异同一个。

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

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]}
}

...尽管嵌套条件语句有点无聊。 :-)

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

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 以便它使用每个 returned 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
}

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

让我们从隔离我们关心的行为开始。我们确定函数 pushupdatepushKey -

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] }

将下面的代码片段展开为 运行 您自己的浏览器中的程序 -

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) 
}

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