Simple Node / Express application, functional way of programming (how to handle side effects in JavaScript?)

There are many good articles on functional programming theory in JavaScript. Some even contain code examples showing the difference between imperative / object-oriented programming and declarative / functional programming. But I haven't found a single show showing simple JavaScript code examples of how to handle side effects in a web application. No real world application can completely eliminate side effects (database calls, console logins, saving to file, drawing to screen, etc.), and I'm having a hard time figuring out how this is done in practice.

There are blog articles and S / O answers (such as this one: How to Perform Side Effects in Purely Functional Programming? ) That deal with the topic of handling side effects in the real world, but they tend to be far from simple, don't include sample code, or include sample code in other languages โ€‹โ€‹(Haskell, Scala, etc.). I haven't found one for Node / JavaScript.

So ... given the following very basic MongoDB Node / Express Application example, what code changes would need to be implemented in order for this piece of code to fully reflect modern functional JavaScript programming techniques. Especially when it comes to routes / functions that handle database calls. I hope your answers help me and others to better understand the practical application of the concept of "side-effect avoidance" of functional programming in real JavaScript.

/*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!')
})

      

+3


source to share


1 answer


You may not be able to completely avoid side effects, but you can make an effort to abstract them as much as possible.

For example, the Express framework is inherently required. You are doing type functions res.send()

entirely for your side effects (you don't even care about its return value most of the time).

What you could do (besides using data structures const

for all your declarations, using Immutable.js , Ramda , writing all functions as const fun = arg => expression;

instead const fun = (arg) => { statement; statement; };

, etc.) would be to do a little abstraction about how Express normally works.

For example, you can create functions that take req

as a parameter and return an object that contains the response status, headers, and a stream that will be passed as a body. These functions can be pure functions in a sense that their return value only depends on their argument (the request object), but you still need some wrapper to actually send the response using the inherent API Express imperative. It may not be trivial, but it can be done.

As an example, consider this function, which takes a body as an object to be sent as json:

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

      

It can be used to create route handlers such as:

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 answer:

$ 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 can force the function wrap()

to accept promises for the return value of functions for asynchronous fingerprints, but then it might not be such a side effect:

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

      

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

      

This can be made even more generic if the function that is the argument for wrap

is a generator, which instead of yielding only promises for resolution (for example, usually based on generators), it will give either promises to allow or cut the stream. with some packaging to distinguish them. This is just a basic idea, but it can be expanded much further.

+12


source







All Articles