在函数式编程中操纵状态

Manipulating state in functional programming

我刚开始学习函数式编程,我想知道在下面的情况下我应该怎么做

  1. 假设我们有一些状态A 具有以下结构
let A =  {
   b: {
     ...usefulldata...
   },
   c: [
     { ...entity1... },
     { ...entity2... },
     { ...entity2... },
   ]

}
  1. 我们有一些函数可以从实体中提取一些 属性 我们称之为 extractProperty

  2. 我们的目标是编写函数 handleState ( A -> object with mapped array in 属性 c)

    所以它接收状态A(从第一步)

    然后它应该将所有实体映射到具有函数 extractProperty

    的属性

    最后它应该 return 在 c 属性 中使用新数组的新状态像这样

{
   b: {
     ...usefulldata...
   },
   c: [
     propertyOfentity1,
     propertyOfentity2,
     propertyOfentity3,
   ]

}

所以我试着写这样的东西

handleState = pipe(
   extractC,
   map(extractProperty),
)

但问题是最后我只得到 c 数组,所以 可能的解决方案是

handleState = pipe(
   extractC,
   (state) => ({
       ...state, //Extract old state to new state
       c: map(extractProperty)}),
)

但这在函数式编程中是否可行,或者有其他方法可以解决这个问题?

正如 Iven Marquardt 所说,您的方法很好并且遵循函数式编程原则,但可能会重复。我建议改用镜头。

什么是镜头?

What are lenses used/useful for? 问题中对镜头有很好的解释。即使问题集中在 Haskell,答案和解释仍然主要适用于 JS。

从本质上讲,镜头是一种获取、设置和修改数据结构部分(例如,对象的属性或数组的元素)的方法。这将 {...state, c: map(extractProperty)})} 抽象为 modify(cLens, map(extractProperty), state).

之类的东西

一个简单的实现

虽然我建议使用库使事情变得更容易,但了解镜头工作原理的基础知识可能会有所帮助。

const get = lens => s => lens.get(s)
const modify = lens => f => lens.modify(f)
const set = lens => a => lens.modify(() => a)

// You can also compose lenses:
const composeLens = (sa, ab) => ({
  get: s => ab.get(sa.get(s)),
  modify: f => sa.modify(ab.modify(f))
})

const propLens = prop => ({
  get: ({[prop]: a}) => a,
  modify: f => ({[prop]: a, ...rest}) => ({...rest, [prop]: f(a)})
})

const idxLens = i => ({
  get: arr => arr[i],
  modify: f => arr => [...arr.slice(0, i), f(arr[i]), ...arr.slice(i + 1)]
})

const cLens = propLens('c')
const headLens = idxLens(0)
const headOfC = composeLens(cLens, headLens)

const state = {b: 0, c: [1, 2, 3]}
console.log(get(headOfC)(state)) // 1
console.log(set(headOfC)(4)(state)) // {b: 0, c: [4, 2, 3]}
console.log(modify(headOfC)(x => 5 * x)(state)) // {b: 0, c: [5, 2, 3]}

// your example:
modify(cLens)(map(extractProperty))(A)
// or alternatively
cLens.modify(map(extractProperty))(A)

使用库

虽然上述实现有效,但还有其他更通用的镜头实现,可以促进棱镜(可以 return 一个可选值)和遍历(可以 return 一个应用函子值)。

正如 Iven Marquardt 所建议的,Ramda 是一个很好的函数式编程库,包括镜头:

const cLens = R.lensProp('c')
const headLens = R.lensIndex(0)
const headOfC = R.compose(cLens, headLens)

const state = {b: 0, c: [1, 2, 3]}
console.log(R.view(headOfC, state)) // 1
console.log(R.set(headOfC, 4, state)) // {b: 0, c: [4, 2, 3]}
console.log(R.over(headOfC, x => 5 * x, state)) // {b: 0, c: [5, 2, 3]}
<script src="https://cdn.jsdelivr.net/npm/ramda@0.27.0/dist/ramda.min.js"></script>

就我个人而言,我更喜欢 TypeScript,并且更喜欢 monocle-ts(您仍然可以在普通 JavaScript 中使用它),因为它具有更好的 TypeScript 支持并与 fp-ts 很好地集成:

import {Lens} from 'monocle-ts'
import {indexReadonlyArray} from 'monocle-ts/lib/Index/ReadonlyArray'

interface State {
  readonly b: number
  readonly c: readonly number[]
}

const cLens = Lens.fromProp<State>()('c')
const headLens = indexReadonlyArray<number>().index(0)
const headOfC = cLens.composeOptional(headLens)

const state = {b: 0, c: [1, 2, 3]}
console.log(headOfC.getOption(state)) // {_tag: 'Some', value: 1}
console.log(headOfC.set(4)(state)) // {b: 0, c: [4, 2, 3]}
console.log(headOfC.modify(x => x * 5)(state)) // {b: 0, c: [5, 2, 3]}