25
votes

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 programming and declarative/functional programming. But I have found none that show, with simple JavaScript code examples, how to handle side-effects in a web app. No real world application can entirely avoid side-effects (database calls, logging to console, saving to a file, drawing to the screen etc.) and I have a hard time figuring out how it is done in practice.

There are blog articles and S/O answers (like this one: How to perform side-effects in pure functional programming?) that touch on the subject of handling side-effects in the real world but they are usually far from simple, don't include code example or include code example in other languages (Haskell, Scala, etc.). I haven't found one for Node/JavaScript.

So... given the following very simple example Node/Express app with MongoDB database, what code changes must be implemented so that this piece of code fully reflect current JavaScript functional programming best practices. Especially when it comes to the routes/functions handling database calls. I'm hoping your answers will help me, and others, better understand the practical application of the 'avoiding side-effects' concept of Functional Programming in real-world 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!')
})
1
This is an ill-posed problem. As you said, "what would the code look like once fully converted to functional programming" - the question you asked - is meaningless because "No real world application can entirely avoid side-effects". The code you have above is full of side-effects (logging to console, sending greetings, connecting to a database). You already avoid loops and use .find and lambda functions, so the parts which are not inherently stateful are already functional. It's not clear what you want from an answer. (I didn't downvote this question, but this might be why someone did). - anandsun
@anandsun thanks for the feedback. I will gladly edit the question to make it better. Maybe you can help me do that. I was under the impression that the code example from my question did not reflect JavaScript Functional Programming best practices. Everything I read about FP makes it sound like you have to use streams and monads and all these other complicated things to handle side-effects and state changes in your application. I'm wondering what that actually looks like in JavaScript. Are you saying that the code I supplied is as good as it gets when it comes to FP in JavaScript? - snowfrogdev
re: "you have to use streams and monads and all these other complicated things." Eww, no! This is a common misconception that pushes people away from FP because they think it is too complicated. I love FP and use Scala at work, but I don't agree that you HAVE to use monads and streams. See for example fp-is-not-the-answer. Monads > nulls. Streams are nice. But to start, push statefulness to the edges of your code, split your logic up into small functions, use standard library functions, avoid side effect in functions, etc. - anandsun
Another thing is a question like this might have more success on StackExchange or elsewhere, because you're not asking for how to get something working, but advice on style, which is often discouraged on StackOverflow because it's subjective. - anandsun
@neoflash Essentially it means that you defer impure computations with thunks (functions without arguments), build your pure function compositions around them and leave it to the caller to actually execute them. By doing so you defer the effects, or more allegorical, you push it to the edges of your application. - user6445533

1 Answers

33
votes

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.