Reputation: 1258
How does one deal with persistent objects like socket connections in functional programming?
We have several functions like this
function doSomething(argument1, argument2) {
const connection = createConnection(argument1); // connection across the network
const result = connection.doSomething();
connection.close()
return result;
}
Each one recreating the connection object, which is a fairly expensive operation. How could one persist a connection like that in functional programming? Currently, we simply made the connection global.
Upvotes: 1
Views: 352
Reputation: 21926
Your program is going to have state. Always. Your program is going to do some I/O. Pretty much always. Functional programming is not about not doing those things, it's about controlling them: doing them in a way where such things that tend to complicate code maintenance and reason-ability are reasonably confined.
As for your particular function, I would argue that it has a problem: you've conflated creating a connection with doing something with that connection.
You probably want to start with something more like this:
const createConn = (arg) => createConnection(arg);
const doSomething = (conn, arg) => conn.doSomething(arg);
Note that this is easier to test: you can pass a mock in a unit test in a way that you can't with your original. An even better approach would be to have a cache:
const cache = new WeakMap();
const getConn = (arg) => {
const exists = cache.get(arg);
let conn;
if (!exists) {
conn = createConnection(arg);
cache.set(arg, conn);
} else {
conn = exists;
}
return conn;
}
Now your getConn
function is idempotent. And a better approach still would be to have a connection pool:
const inUse = Symbol();
const createPool = (arg, max=4) => {
// stateful, captured in closure, but crucially
// this is *opaque to the caller*
const conns = [];
return async () => {
// check the pool for an available connection
const available = conns.find(x => !x[inUse]);
if (available) {
available[inUse] = true;
return available;
}
// lazily populate the cache
if (conns.length < max) {
const conn = createConn(arg);
conn.release = function() { this[inUse] = false };
conn[inUse] = true;
conns.push(conn);
return conn;
}
// If we don't have an available connection to hand
// out now, return a Promise of one and
// poll 4 times a second to check for
// one. Note this is subject to starvation even
// though we're single-threaded, i.e. this isn't
// a production-ready implementation so don't
// copy-pasta.
return new Promise(resolve => {
const check = () => {
const available = conns.find(x => !x[inUse]);
if (available) {
available[inUse] = true;
resolve(available);
} else {
setTimeout(check, 250);
}
};
setTimeout(check, 250);
});
};
}
Now the details of the creation are abstracted away. Note that this is still stateful and messy, but now the consuming code can be more functional and easier to reason about:
const doSomething = async (pool, arg) => {
const conn = await pool.getConn();
conn.doSomething(arg);
conn.release();
}
// Step 3: profit!
const pool = createPool(whatever);
const result = doSomething(pool, something);
As a final aside, when trying to be functional (especially in a language not built on that paradigm) there is only so much you can do with sockets. Or files. Or anything else from the outside world. So don't: don't try to make something inherently side-effective functional. Instead put a good API on it as an abstraction and properly separate your concerns so that the rest of your code can have all of the desirable properties of functional code.
Upvotes: 1