简体   繁体   中英

Manipulating state in functional programming

I just start learning functional programming, and I'm wondering what I should do in the following case

  1. Assume we have some state A with the following structure
let A =  {
   b: {
     ...usefulldata...
   },
   c: [
     { ...entity1... },
     { ...entity2... },
     { ...entity2... },
   ]

}
  1. we have some function that extract some property from entity let's called it extractProperty

  2. Our target is to write function handleState ( A -> object with mapped array in property c)

    so it receive state A (from first step)

    then it should map all entities to properties with function extractProperty

    and finally it should return new state with new array in c property like this

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

}

so I tried to write something like this

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

but the problem is finally I get only c array, so the possible solution is

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

but is this okay in functional programming, or there is another way to solve this?

Like Iven Marquardt said, your approach is fine and adheres to functional programming principles, but can get repetitive. I would suggest using lenses instead.

What are lenses?

There's a good explanation of lenses in the What are lenses used/useful for? question. Even though the question is focused on Haskell, the answers and explanations are still mostly applicable to JS.

Essentially, a lens is a way to get, set, and modify parts of a data structure (eg properties of an object or elements of an array). This abstracts over the {...state, c: map(extractProperty)})} into something like modify(cLens, map(extractProperty), state) .

A simple implementation

While I recommend using a library to makes things easier, understanding the basics of how lenses work can be helpful.

 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)

Using libraries

While the above implementation works, there are other implementations of lenses that are more general and facilitate things such as prisms (can return an optional value) and traversals (can return an applicative functor of values).

As Iven Marquardt suggested, Ramda is a good library for functional programming that includes lenses:

 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>

Personally, I'm more of a TypeScript person and prefer monocle-ts (which you can still use in plain JavaScript) as it has better TypeScript support and integrates nicely with 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]}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM