MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

App Simple Node/Express, il modo di programmazione funzionale (Come gestire gli effetti collaterali in JavaScript?)

Non sarai in grado di evitare del tutto gli effetti collaterali, ma puoi fare uno sforzo per astrarli al massimo, ove possibile.

Ad esempio, il framework Express è intrinsecamente imperativo. Esegui funzioni come res.send() interamente per i loro effetti collaterali (il più delle volte non ti interessa nemmeno il suo valore di ritorno).

Cosa potresti fare (oltre a usare const per tutte le tue dichiarazioni, utilizzando Immutable.js strutture dati, Ramda , scrivendo tutte le funzioni come const fun = arg => expression; invece di const fun = (arg) => { statement; statement; }; ecc.) sarebbe fare una piccola astrazione su come funziona solitamente Express.

Ad esempio potresti creare funzioni che accettano req come parametro e restituiscono un oggetto che contiene lo stato della risposta, le intestazioni e un flusso da reindirizzare come corpo. Quelle funzioni potrebbero essere funzioni pure, nel senso che il loro valore restituito dipende solo dal loro argomento (l'oggetto della richiesta), ma avresti comunque bisogno di un wrapper per inviare effettivamente la risposta usando l'API intrinsecamente imperativa di Express. Potrebbe non essere banale ma si può fare.

Ad esempio, considera questa funzione che prende il corpo come oggetto da inviare come json:

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

Potrebbe essere utilizzato per creare gestori di percorsi come questo:

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

utilizzando una funzione che restituisce una singola espressione senza effetti collaterali.

Esempio completo:

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

Testare la risposta:

$ 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}

Ovviamente questa è solo un'idea di base. Potresti creare il wrap() funzione accetta promesse per il valore restituito delle funzioni per operazioni asincrone, ma probabilmente non sarà così privo di effetti collaterali:

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

e un gestore:

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

Ho usato .then() invece di async /await nel gestore stesso per farlo sembrare più funzionale, ma può essere scritto come:

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

Potrebbe essere reso ancora più universale se la funzione che è un argomento per wrap sarebbe un generatore che invece di produrre solo promesse di risoluzione (come fanno di solito le coroutine basate su generatori) produrrebbe promesse di risoluzione o mandrini per lo streaming, con qualche avvolgimento per distinguere i due. Questa è solo un'idea di base, ma può essere ulteriormente ampliata.