About the composition of functions in JavaScript
Let's fantasize about the functional composition, as well as clarify the meaning of the composition / pipeline operator.
TL; DR
Compose functions like a boss:
Popular implementations compose
- when calling, they create new and new functions based on recursion, what are the disadvantages and how to get around it.
You can consider the compose function as a pure function that depends only on the arguments. Thus, by composing the same functions in the same order, we must get an identical function, but in the JavaScript world this is not the case. Any call to compose returns a new function, this leads to the creation of more and more new functions in memory, as well as to the questions of their memoization, comparison and debugging.
Need to do something.
Motivation
- Get associative identity:
It is very desirable not to create new objects and reuse the previous results of the compose function. One of the problems React of the developer - the implementation of shallowCompare, working with the result of the composition of functions. For example, the composition of sending an event with a callback will always create a new function, which will lead to updating the value of the property.
Popular implementations of the composition do not have the identity of the return value.
Partially, the question of the identity of the compositions can be solved by memorizing the arguments. However, the question of associative identity remains:
import {memoize} from'ramda'const memoCompose = memoize(compose)
memoCompose(a, b) === memoCompose(a, b)
// да, аргументы одинаковые
memoCompose(memoCompose(a, b), c) === memoCompose(a, memoCompose(b, c))
// нет, мемоизация не помогает так как аргументы разные
- Simplify composition debugging:
Of course, using tap functions helps with debugging functions that have a single expression in the body. However, it is advisable to have as flat a call stack as possible for debugging.
- Get rid of the recursion overhead projector:
The recursive implementation of a functional composition has an overhead, creating new elements in the call stack. When calling a composition of 5 or more functions, this is clearly visible. And using functional approaches in the development it is necessary to build compositions from dozens of very simple functions.
Decision
Make a monoid (or semigruppo with category specification support) in terms of fantasy-land:
import compose, {identity} from 'lazy-compose'
import {add} from 'ramda'
const a = add(1)
const b = add(2)
const c = add(3)
test('Laws', () => {
compose(a, compose(b, c)) === compose(compose(a, b), c) // ассоциативность
compose(a, identity) === a //right identity
compose(identity, a) === a //left identity
}
Use cases
- Useful in memosizing composite compositions when working with redaks. For example for redux / mapStateToProps and
reselect. - The composition of the lenses.
You can create and reuse strictly equivalent lenses focused in one and the same place.
import {lensProp, memoize} from 'ramda'
import compose from 'lazy-compose'
const constantLens = memoize(lensProp)
const lensA = constantLens('a')
const lensB = constantLens('b')
const lensC = constantLens('c')
const lensAB = compose(lensB, lensA)
console.log(
compose(lensC, lensAB) === compose(lensC, lensB, lensA)
)
- Memotized callbacks, with the possibility of composition up to the final function of sending an event.
In this example, the same callback will be passed to the list items.
```jsx
import {compose, constant} from './src/lazyCompose'
// constant - returns the same memoized function for each argrum
// just like React.useCallback
import {compose, constant} from 'lazy-compose'
const List = ({dispatch, data}) =>
data.map( id =>
<Button
key={id}
onClick={compose(dispatch, makeAction, contsant(id))}
/>
)
const Button = React.memo( props =>
<button {...props} />
)
const makeAction = payload => ({
type: 'onClick',
payload,
})
```
Lazy composition of React components without creating higher order components. In this case, the lazy composition will collapse the array of functions, without creating additional closures. This issue is of concern to many developers who use the recompose library.
import {memoize, mergeRight} from 'ramda' import {constant, compose} from './src/lazyCompose' const defaultProps = memoize(mergeRight) const withState = memoize( defaultState => props => { const [state, setState] = React.useState(defaultState) return {...props, state, setState} } ) const Component = ({value, label, ...props)) => <label {...props}>{label} : {value}</label> const withCounter = compose( ({setState, state, ...props}) => ({ ...props value: state, onClick: compose(setState, constant(state + 1)) }), withState(0), ) const Counter = compose( Component, withCounter, defaultProps({label: 'Clicks'}), )
Monads and applicatives (in terms of fantasy-land) with strict equivalence through caching the result of the composition. If inside the type constructor to access the dictionary of previously created objects, you get the following:
type Info = {
age?: number
}
type User = {
info?: Info
}
const mayBeAge = LazyMaybe<Info>.of(identity)
.map(getAge)
.contramap(getInfo)
const age = mayBeAge.ap(data)
const maybeAge2 = LazyMaybe<User>.of(compose(getAge, getInfo))
console.log(maybeAge === maybeAge2)
// создав эквивалентные объекты, мы можем мемоизировать их вместе
// переиспользовать как один объект и бонусом получить короткий стек вызовов
I have been using this approach for a long time, I have created a repository here .
NPM package: npm i lazy-compose
.
It is interesting to get feedback about the limitation of the cache created in runtime functions dependent on the circuit.
UPD
I foresee the obvious questions:
Yes, you can replace Map with WeakMap.
Yes, you need to make it possible to connect a third-party cache as middleware.
It is not necessary to arrange a debate on caches, there is no perfect caching strategy.
Why tail and head, if everything is in the list - tail and head, part of the implementation with memoization based on the parts of the composition, and not each function separately.