📜 ⬆️ ⬇️

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:
image
Popular compose implementations - when calling create new and new functions based on recursion, what are the disadvantages here and how to get around it.


You can consider the compose function as a pure function that depends only on the arguments. Thus 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



It is highly 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)) // ,        


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.



A recursive implementation of a functional composition has an overhead, creating new elements in the call stack. When you call 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



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) ) 


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, }) ``` 


  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 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.


')

Source: https://habr.com/ru/post/432196/


All Articles