À propos de la composition des fonctions en JavaScript

Imaginons le sujet de la composition fonctionnelle et clarifions la signification de l'opérateur de composition / pipeline.


TL; DR
Composez des fonctions comme un boss:
image
Les implémentations populaires de compose - lorsqu'elles sont appelées, elles créent de nouvelles et nouvelles fonctions basées sur la récursivité, quels sont les inconvénients et comment contourner ce problème.


Vous pouvez considérer la fonction de composition comme une fonction pure qui ne dépend que des arguments. Ainsi, en composant les mêmes fonctions dans le même ordre, nous devrions obtenir une fonction identique, mais dans le monde JavaScript, ce n'est pas le cas. Tout appel à composer - renvoie une nouvelle fonction, cela conduit à la création de plus en plus de nouvelles fonctions en mémoire, ainsi qu'aux problèmes de leur mémorisation, comparaison et débogage.
Il faut faire quelque chose.


La motivation


  • Obtenez une identité associative:

Il est fortement conseillé de ne pas créer de nouveaux objets et de réutiliser les résultats précédents de la fonction de composition. L'un des problèmes des développeurs de React est l'implémentation de shallowCompare, qui fonctionne avec le résultat de la composition des fonctions. Par exemple, la composition de l'envoi d'un événement avec un rappel créera toujours une nouvelle fonction, ce qui entraînera une mise à jour de la valeur de la propriété.


Les implémentations populaires d'une composition n'ont pas d'identité de valeur de retour.
En partie, le problème de l'identité de la chanson peut être résolu en mémorisant les arguments. Cependant, la question de l'identité associative demeure:


 import {memoize} from 'ramda' const memoCompose = memoize(compose) memoCompose(a, b) === memoCompose(a, b) // ,   memoCompose(memoCompose(a, b), c) === memoCompose(a, memoCompose(b, c)) // ,        

  • Simplifiez le débogage de la composition:

Bien sûr, l'utilisation des fonctions tap aide à déboguer les fonctions qui ont une seule expression dans le corps. Cependant, il est souhaitable d'avoir une pile d'appels aussi plate que possible pour le débogage.


  • Débarrassez-vous des frais généraux liés à la récursivité:

L'implémentation récursive de la composition fonctionnelle a un surcoût, créant de nouveaux éléments dans la pile d'appels. Lorsque vous appelez une composition de 5 fonctions ou plus, cela est clairement visible. Et en utilisant des approches fonctionnelles en développement, il est nécessaire de construire des compositions à partir de dizaines de fonctions très simples.


Solution


Faire un monoïde (ou un semi-groupe avec le support de la spécification de catégorie) en termes de 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 } 

Cas d'utilisation


  • Utile pour mémoriser des compositions composites lorsque vous travaillez avec des éditeurs. Par exemple pour redux / mapStateToProps et
    resélectionnez.
  • La composition des lentilles.

Vous pouvez créer et réutiliser des objectifs strictement équivalents focalisés au même endroit.


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

  • Rappels mémorisés, avec la possibilité de composer jusqu'à la fonction finale d'envoi d'un événement.

Dans cet exemple, le même rappel sera envoyé aux éléments de la liste.


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

  • Composition paresseuse des composants React sans créer de composants d'ordre supérieur. Dans ce cas, la composition paresseuse réduira le tableau de fonctions, sans créer de fermetures supplémentaires. Cette question inquiète de nombreux développeurs utilisant la bibliothèque de recomposition.


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

  • Monades et applicatifs (en termes de fantasy-land) avec une stricte équivalence grâce à la mise en cache du résultat de la composition. Si vous accédez au dictionnaire des objets créés précédemment dans le constructeur de type, vous obtenez les éléments suivants:



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

J'utilise cette approche depuis longtemps, j'ai conçu le référentiel ici .
Package NPM: npm i lazy-compose .


Il est intéressant d'obtenir des commentaires sur la fermeture du cache des fonctions créées lors de l'exécution en fonction des fermetures.


UPD
Je prévois des questions évidentes:
Oui, vous pouvez remplacer Map par WeakMap.
Oui, vous devez permettre de connecter un cache tiers en tant que middleware.
Vous ne devriez pas organiser un débat sur le sujet des caches; il n'y a pas de stratégie de mise en cache idéale.
Pourquoi queue et tête, si tout est dans la liste - queue et tête, une partie de la mise en œuvre avec mémorisation basée sur des parties de la composition, et non chacune fonctionne séparément.

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


All Articles