JavaScript镜头:有效修改不变数据的指南
#javascript #教程 #功能

近年来,软件行业经历了向功能编程的范式。

编程中的范式转移是指我们处理代码的方式的变化。与普遍的信念相反,范式没有带来新功能,它具有一系列局限性。这些限制在接受时可以带来编写更好的软件的好处。

这是我的意思:

  • 汇编语言是一种低级语言,没有限制,可以无限制地访问内存位置以及操纵字节,指针和代码流的能力。
  • 结构编程介绍了代码块的概念,并限制了我们如何处理迭代和子例程。
  • 面向对象的编程(OOP)通过控制如何通过封装和抽象访问对象的内部内存来增加进一步的限制。
  • 功能编程,将我们限制在使用纯函数和不变结构的情况下,从而在代码中提高清晰度和理解。这在并行和并发编程中特别有用,因为纯函数和不可变的对象最大程度地减少了意外修改的机会。

与不变的结构合作在功能编程中可以提出挑战,尤其是在缺乏不可超不可分率工具的JavaScript中。这要求开发人员承担确保不变性的责任。简单的更新(例如通过破坏来更改价值)很简单:

const person = {
  name: 'John',
  age: 25
}

const newPerson = { ...person, age: 26 }

更新阵列也很简单:

const users = [...]

const newUsers = users.concat([newPerson]) // or [...users, newPerson]

但是,随着嵌套结构的复杂性增加,使用更新数据创建新副本变得更加困难。例如,更新像person.address这样的嵌套对象:

const person = {
  name: 'John',
  address: {
    street: '1 Water Ln',
    city: 'London'
    index: 'NW1 8NX'
  }
}

const newPerson = {
  ...person,
  address: {
    ...person.address,
    index: 'NW1 8NZ'
 }
}

或在数组中更新对象:

const users = [...]

const newUsers = [
  {
    ...users[0],
    address: {
       ...users[0].address,
       index: 'NW1 8NZ'
  },
  ...users.slice(1)
]

结构嵌套得越多,在不突变原始数据的情况下进行更新的挑战就越大。

JavaScript不支持修改不可变数据的简便方法。 - 也许您想

那是什么解决方案?我们应该放弃纯粹功能和不变结构的概念吗?我们应该避免嵌套对象和数组吗?是否有必要切换到其他编程语言?

对每个的答案是否定的。今天,我想介绍一个可以解决这个问题的概念。这个概念在2010年出现,使用扩展库Lens用于Haskell编程语言。

不要被Haskell吓倒,您将在片刻之内意识到镜头概念的实用性 - me

Lens允许您专注于不可变的数据结构的特定部分,作为getter和setter的功能等效。它可以描述为具有两个功能的通用类:getter和setter。

class Lens<S, A> {
  getter: (S) => A
  setter: (A, S) => S
}

S代表不变的对象,而A是该对象内的焦点值。 getter函数定义了如何查看值,而设置器是更新对象并创建S的新实例的纯函数。

为什么SA?但是,我无法发现S代表“源”,而A代表“焦点”。字母F通常保留用于表示高阶类型,因此A被选为简单类型的共同占位符。

可以使用Ramda之类的库来完成JavaScript中镜头的实现,该库提供了用于在对象和数组上构造镜头的功能。 Ramda的R.lens函数通过提供Getter和Setter函数来创建镜头,而R.lensPropR.lensIndex功能为常用的Getters和Setters提供快捷方式。

const nameLens = R.lens(/*getter*/R.prop('name'), /*setter*/R.assoc('name'))
// use of shortcuts
const nameLens = R.lensProp('name')
const firstLens = R.lensIndex(0)

所以现在我们有了镜头,我们该怎么办?镜头为我们提供了检索(使用R.view),修改(使用R.set)或更新(使用R.over)属性的能力。

const persons = [
  {name: 'John'},
  {name: 'Steve'}
]

const updateName = person =>
  R.over(nameLens, name => name + ' Smith', person)

R.over(firstLens, updateName, persons) // [{"name": "John Smith"}, {"name": "Steve"}]

persons // [{"name": "John"}, {"name": "Steve"}]

在上面的示例中,我们利用了一个中间功能来更新数组的值。但是您知道镜头可以组成吗?让我们写一个辅助功能以组合两个镜头,使我们可以专注于带有单个镜头的结构的较小部分。

function andThen<S, S1, A>(lens1: Lens[S, S1], lens2: Lens[S1, A]): Lens[S, A] {

  const getter = R.pipe(
    R.view(lens1),
    R.view(lens2)
  )

  const setter = (a, s) => 
    R.over(lens1, R.set(lens2, a), s)

  return R.lens(getter, setter)
}

这就是我们可以使用该函数的方式:

const nameLens = R.lensProp('name')
const firstLens = R.lensIndex(0)

const nameInFirstLens = andThen(firstLens, nameLens)

const persons = [
  {name: 'John'},
  {name: 'Steve'}
]

R.over(nameInFirstLens, name => name + ' Smith', persons) // [{"name": "John Smith"}, {"name": "Steve"}]
persons // [{"name": "John"}, {"name": "Steve"}]

让我们分解andThen构图的工作方式。

我们将从Getter方面开始。我们有一个顶级镜头镜头1,该镜头侧重于S的内部结构S1和另一个镜头镜头2,该镜头镜头的重点是S1的内部值A。

Lens composition

这是实现Getter的方式:

const getter = R.pipe(
   R.view(lens1),
   R.view(lens2)
 )

R.pipe按顺序运行函数,从一个函数作为输入输出的输出。在这种情况下,getter首先使用lens1查看具有内部结构的值,然后使用lens2查看该结构中的值。

设置器实现更为复杂:

const setter = (a, s) => 
    R.over(lens1, R.set(lens2, a), s)

设置器是一个获取两个输入(A, S)并返回S的函数。让我们从检查R.set(lens2, a)开始,该R.set(lens2, a)创建了一个更新函数S1 => S1,该功能将lens2焦点的值更新为a

R.over函数然后使用lens1更新Haskell中的值S1,以关注它及其由R.set创建的更新函数。 R.over的签名是<S, S1>(Lens[S, S1>, S1 => S1, S) => S

您可以从ramda中用R.compose简化镜头组成:

const nameLens = R.lensProp('name')
const firstLens = R.lensIndex(0)

const nameInFirstLens = R.compose(firstLens, nameLens)

andThen描述的镜头组成方法和带有R.compose的镜头组成方法有所不同,但是最终结果是相同的,无需担心。

对于特定对象的组成,您可以使用R.lensPath

const addressLens = R.lensProp('address')
const zipLens = R.lensProp('zip')

const zipInAddressLens = R.compose(addressLens, zipLens)
const zipInAddressLens2 = R.lensPath(['address', 'zip'])

也可以编写多个更新:

const nameLens = R.lensProp('name')
const ageLens = R.lensProp('age')
const zipAddressLens = R.lensPath(['address', 'zip'])

const firstLens = R.lensIndex(0)

const nameInFirst = R.compose(firstLens, nameLens)
const ageInFirst = R.compose(firstLens, ageLens)
const zipInFirst = R.compose(firstLens, zipAddressLens)

const persons = [
  {name: 'John', age: 25, address: { zip: 'NW1 8NZ' } },
  {name: 'Steve', age: 35, address: { zip: 'NW1 8NZ' } }
]

R.compose(
  R.set(ageInFirst, 30),
  R.over(nameInFirst, name => name + ' Smith'),
  R.set(zipInFirst, 'NW1 8NX')
)(persons)
/*
  [
    {
        address: {
            zip: "NW1 8NX"
        },
        age: 30,
        name: "John Smith"
    },
    {
        address: {
            zip: "NW1 8NZ"
        },
        age: 35,
        name: "Steve"
    }
]
*/

但是,这种方法将数组复制三遍以更新一个人。为了减少这一点,您可以为一个人创建一个组合的更新功能:

const updatePerson = R.compose(
  R.set(ageLens, 30),
  R.over(nameLens, name => name + ' Smith'),
  R.set(zipAddressLens, 'NW1 8NX')
)

然后使用侧重于特定人员的镜头更新数组:

R.over(firstLens, updatePerson, persons)

这会产生相同的结果,但避免创建后来丢弃的中间数组。

现在,您有了一个工具,可以在没有样板的情况下以任何组成结构中的JS更新对象。

如果您有疑问,请在下面留下评论!

关注更多!