L'application Simple Node / Express, la manière de programmation fonctionnelle (Comment gérer les effets secondaires en JavaScript?)

Il existe de nombreux bons articles sur la théorie de la programmation fonctionnelle en JavaScript. Certains contiennent même des exemples de code montrant la différence entre la programmation impérative / orientée objet et la programmation déclarative / fonctionnelle. Mais je n'ai trouvé aucun qui montre, avec des exemples simples de code JavaScript, comment gérer les effets secondaires dans une application Web. Aucune application du monde réel ne peut évidemment éviter les effets secondaires (appels de la base de données, connexion à la console, sauvegarde dans un fichier, dessin sur l'écran, etc.) et j'ai du mal à déterminer comment cela se fait dans la pratique.

Il existe des articles de blog et des réponses S / O (comme celui-ci: comment effectuer des effets secondaires dans la programmation fonctionnelle pure? ) Qui touchent le sujet de la manipulation des effets secondaires dans le monde réel, mais ils sont généralement loin d'être simples, T inclure un exemple de code ou inclure un exemple de code dans d'autres langues (Haskell, Scala, etc.). Je n'ai trouvé aucun pour Node / JavaScript.

Alors … étant donné l'exemple très simple, l'application Node / Express avec la base de données MongoDB, les changements de code doivent être mis en œuvre afin que ce code reflète pleinement les meilleures pratiques de programmation fonctionnelle JavaScript. Surtout en ce qui concerne les itinéraires / fonctions qui gèrent les appels de base de données. J'espère que vos réponses m'aident, et d'autres personnes, à mieux comprendre l'application pratique du concept de la programmation fonctionnelle «éviter les effets secondaires» dans le JavaScript du monde réel.

/*app.js*/ const express = require('express') const app = express() var mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/test'); var greetingSchema = mongoose.Schema({ greeting: String }); var Greeting = mongoose.model('Greeting', greetingSchema); app.get('/', function (req, res) { Greeting.find({greeting: 'Hello World!'}, function (err, greeting){ res.send(greeting); }); }); app.post('/', function (req, res) { Greeting.create({greeting: 'Wasssssssssssuuuuppppp'}, function (err, greeting){ res.send(greeting); }); }); app.listen(3000, function () { console.log('Example app listening on port 3000!') }) 

Vous ne pourrez pas éviter les effets secondaires, mais vous pouvez faire un effort pour les abstraire au maximum, dans la mesure du possible.

Par exemple, le cadre Express est intrinsèquement impératif. Vous exécutez des fonctions comme res.send() entièrement pour leurs effets secondaires (vous ne vous souciez même pas de sa valeur de retour la plupart du temps).

Ce que vous pourriez faire (en plus d'utiliser const pour toutes vos déclarations, en utilisant les structures de données Immutable.js , Ramda , en écrivant toutes les fonctions comme const fun = arg => expression; au lieu de const fun = (arg) => { statement; statement; }; Etc.) serait de faire une petite abstraction sur la façon dont Express fonctionne habituellement.

Par exemple, vous pouvez créer des fonctions qui prennent le paramètre req comme paramètre et renvoient un objet qui contient l'état de la réponse, les en-têtes et un flux à transmettre en tant que corps. Ces fonctions peuvent être des fonctions pures, dans la mesure où leur valeur de retour dépend uniquement de leur argument (l'objet de requête), mais vous auriez encore besoin d'un wrapper pour envoyer la réponse en utilisant l'API de Express, intrinsèquement impérative. Ce n'est peut-être pas trivial, mais cela peut être fait.

À titre d'exemple, considérez cette fonction qui prend le corps comme objet à envoyer en tant que json:

 const wrap = f => (req, res) => { const { status = 200, headers = {}, body = {} } = f(req); res.status(status).set(headers).json(body); }; 

Il pourrait être utilisé pour créer des gestionnaires d'itinéraire comme ceci:

 app.get('/sum/:x/:y', wrap(req => ({ headers: { 'Foo': 'Bar' }, body: { result: +req.params.x + +req.params.y }, }))); 

En utilisant une fonction qui renvoie une seule expression sans effets secondaires.

Exemple complet:

 const app = require('express')(); const wrap = f => (req, res) => { const { status = 200, headers = {}, body = {} } = f(req); res.status(status).set(headers).json(body); }; app.get('/sum/:x/:y', wrap(req => ({ headers: { 'Foo': 'Bar' }, body: { result: +req.params.x + +req.params.y }, }))); app.listen(4444); 

Test de la réponse:

 $ curl localhost:4444/sum/2/4 -v * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 4444 (#0) > GET /sum/2/4 HTTP/1.1 > User-Agent: curl/7.35.0 > Host: localhost:4444 > Accept: */* > < HTTP/1.1 200 OK < X-Powered-By: Express < Foo: Bar < Content-Type: application/json; charset=utf-8 < Content-Length: 12 < ETag: W/"c-Up02vIPchuYz06aaEYNjufz5tpQ" < Date: Wed, 19 Jul 2017 15:14:37 GMT < Connection: keep-alive < * Connection #0 to host localhost left intact {"result":6} 

Bien sûr, ceci n'est qu'une idée de base. Vous pourriez faire en sorte que la fonction wrap() accepte des promesses pour la valeur de retour des fonctions pour les opératures asynchrones, mais cela ne sera pas sans effet contraire aux effets secondaires:

 const wrap = f => async (req, res) => { const { status = 200, headers = {}, body = {} } = await f(req); res.status(status).set(headers).json(body); }; 

Et un gestionnaire:

 const delay = (t, v) => new Promise(resolve => setTimeout(() => resolve(v), t)); app.get('/sum/:x/:y', wrap(req => delay(1000, +req.params.x + +req.params.y).then(result => ({ headers: { 'Foo': 'Bar' }, body: { result }, })))); 

J'ai utilisé .then() au lieu d' async / await dans le gestionnaire lui-même pour le rendre plus fonctionnel, mais il peut être écrit comme .then() :

 app.get('/sum/:x/:y', wrap(async req => ({ headers: { 'Foo': 'Bar' }, body: { result: await delay(1000, +req.params.x + +req.params.y) }, }))); 

Il pourrait être rendu encore plus universel si la fonction qui est un argument à wrap serait un générateur qui, au lieu de donner seulement des promesses à résoudre (comme les corutines basées sur le générateur en général), il produirait soit des promesses de résolution, soit des mandrins à transmettre, Avec un emballage pour distinguer les deux. C'est juste une idée de base, mais cela peut être étendu beaucoup plus loin.