Vous ne pourrez pas éviter complètement les effets secondaires, mais vous pouvez faire un effort pour les éliminer au maximum lorsque cela est possible.
Par exemple, le framework 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 Immutable.js
structures de données, Ramda
, en écrivant toutes les fonctions sous la forme const fun = arg => expression;
au lieu de const fun = (arg) => { statement; statement; };
etc.) serait de faire une petite abstraction sur le fonctionnement habituel d'Express.
Par exemple, vous pouvez créer des fonctions qui prennent req
en tant que paramètre et renvoie un objet qui contient l'état de la réponse, les en-têtes et un flux à canaliser en tant que corps. Ces fonctions pourraient être des fonctions pures dans le sens où leur valeur de retour dépend uniquement de leur argument (l'objet de la requête), mais vous auriez toujours besoin d'un wrapper pour envoyer la réponse à l'aide de l'API intrinsèquement impérative d'Express. Ce n'est peut-être pas anodin, mais c'est faisable.
À titre d'exemple, considérons 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 de route comme celui-ci :
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, ce n'est qu'une idée de base. Vous pourriez faire le wrap()
la fonction accepte les promesses pour la valeur de retour des fonctions pour les opérations asynchrones, mais cela ne sera sans doute pas aussi sans effet secondaire :
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 de async
/await
dans le gestionnaire lui-même pour le rendre plus fonctionnel, mais il peut être écrit comme :
app.get('/sum/:x/:y', wrap(async req => ({
headers: { 'Foo': 'Bar' },
body: { result: await delay(1000, +req.params.x + +req.params.y) },
})));
Cela pourrait être rendu encore plus universel si la fonction qui est un argument de wrap
serait un générateur qui, au lieu de ne donner que des promesses de résolution (comme le font généralement les coroutines basées sur un générateur), il donnerait soit des promesses de résolution, soit des mandrins à diffuser, avec un emballage pour distinguer les deux. Ce n'est qu'une idée de base, mais elle peut être étendue beaucoup plus loin.