楔子
在 functional programming 中要進行對 store 的設定時,可以用 lens 的方式來進行設定,如果就像一個吸管直接定位來改變值,進行值也可以用函數來取代。
lens laws
laws
- view(lens, set(lens, store, a)) = a 如果你改變了 store 某個值之後再立刻利用 lens 來取 store 的值,二個會相等(白話的意思就是你改變了 store 的值再拿出來看就是那個值)
- set(lens, b, set(lens, store, a)) = set(lens, b, store) 如果你改變了 store 的值為 a 又立馬改變值為 b,等同於直接改變值為 b(白話的意思就是你把一個值設了二次,最後的結果是第二次的值)
- set(lens, view(lens, store), store) = store 如果你用 lens 拿了某個值出來再同時 set 在 store 的 lens 位置的值,store 和原來的一致(白話文就是你取個 lens 的值拿出來的值再設到同一個 lens,該 store 維持不變)
const view = (lens, store) => lens.view(store)
const set = (lens, value, store) => lens.set(value, store)
const lensProp = prop => ({
view: store => store[prop],
set: (value, store) => ({
...store,
[prop]: value
})
})
const fooStore = {
a: 'foo',
b: 'bar'
}
const aLens = lensProp('a')
const bLens = lensProp('b')
const a = view(aLens, fooStore)
const b = view(bLens, fooStore)
const bazStore = set(aLens, 'baz', fooStore)
console.log( view(aLens, bazStore) ) // 'baz'
// prove lens laws
const store = fooStore
{
// view(lens, set(lens, store, a)) = a
const lens = lensProp('a')
const value = 'baz'
const a = value
const b = view(lens, set(lens, value, store))
console.log(a,b) // 'baz' 'baz'
}
{
// set(lens, b, set(lens, store, a)) = set(lens, b, store)
const lens = lensProp('a')
const a = 'bar'
const b = 'baz'
const r1 = set(lens, b, set(lens, a, store))
const r2 = set(lens, b, store)
console.log(r1, r2) // {a: 'baz', b: 'bar'} {a: 'baz', b: 'bar'}
}
{
// set(lens, view(lens, store), store) = store
const lens = lensProp('a')
const r1 = set(lens, view(lens, store), store)
const r2 = store
console.log(r1, r2) // {a: 'foo', b: 'bar'} {a: 'foo', b: 'bar'}
}
Composing lenses
ramda 的 lensProp 就是可以把 array 轉換成巢狀的效果
lensProp String → Lens s a Lens s a = Functor f => (a → f a) → s → f s
import {lensProp} from 'ramda'
const lensProps = [1, 'bar', 'foo']
const lenses = lensProps.map(lensProp)
const truth = compose(...lenses)
const obj = {
foo: {
bar: [false, true]
}
}
console.log(truth(obj)) // true
Over
在 lens laws 中,可以使用 function 來取代設定的值,甚至可以進行 compose
// over = (lens, f: a => a, store) => store
const over = (lens, f, store) => set(lens, f(view(lens, store)), store)
const uppercase = x => x.toUpperCase()
console.log(
over(aLens, uppercase, store) // {a: "FOO", b: "bar"}
)
如果該 over 的 f 為 id function 則原 store 不變
const id = x => x
const lens = aLens
const a = over(lens, id, store)
const b = store
console.log(a, b)
可以使用 curry 的功能
import {curry} from 'ramda'
const over = curry(
(lens, f, store) => set(lens, f(view(lens, store)), store)
)
{
// over(lens, f) after over(lens g) is the same as over(lens, compose(f,g))
const lens = aLens
const store = {a: 20}
const g = n => n + 1
const f = n => n * 2
const a = compose(over(lens, f), over(lens, g))
const b = over(lens, compose(f,g))
console.log(
a(store), // {a: 42}
b(store) // {a: 42}
)
}
參考資料