Simple Node/Express app, the functional programming way (How to handle side-effects in JavaScript?)

后端 未结 1 518
时光说笑
时光说笑 2021-02-01 18:04

There are many good articles about the theory of functional programming in JavaScript. Some even contain code examples showing the difference between imperative/object-oriented

1条回答
  •  太阳男子
    2021-02-01 18:15

    You will not be able to avoid side effects entirely but you can make some effort to maximally abstract them away where possible.

    For example the Express framework is inherently imperative. You run functions like res.send() entirely for their side effects (you don't even care about its return value most of the time).

    What you could do (in addition to using const for all your declarations, using Immutable.js data structures, Ramda, writing all functions as const fun = arg => expression; instead of const fun = (arg) => { statement; statement; }; etc.) would be to make a little abstraction on how Express usually works.

    For example you could create functions that take req as parameter and return an object that contains response status, headers and a stream to be piped as body. Those functions could be pure functions in a sense that their return value depend only on their argument (the request object) but you would still need some wrapper to actually send the response using the inherently imperative API of Express. It may not be trivial but it can be done.

    As an example consider this function that takes body as an object to send as json:

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

    It could be used to create route handlers like this:

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

    using a function that returns a single expression with no side effects.

    Complete example:

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

    Testing the response:

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

    Of course this is just a basic idea. You could make the wrap() function accept promises for the return value of the functions for async oprations, but then it will arguably not be so side-effect free:

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

    and a handler:

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

    I used .then() instead of async/await in the handler itself to make it look more functional, but it can be written as:

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

    It could be made even more universal if the function that is an argument to wrap would be a generator that instead of yielding only promises to resolve (like the generator-based coroutines usually do) it would yield either promises to resolve or chucks to stream, with some wrapping to distinguish the two. This is just a basic idea but it can be extended much further.

    0 讨论(0)
提交回复
热议问题