如何处理函数式编程中的可重用连接对象(in Node.js)?

How to deal with re-usable connection objects in functional programming (in Node.js)?

如何在函数式编程中处理诸如套接字连接之类的持久对象?

我们有几个这样的功能

function doSomething(argument1, argument2) {
  const connection = createConnection(argument1); // connection across the network
  const result = connection.doSomething();
  connection.close()
  return result;
}

每个都重新创建连接对象,这是一个相当昂贵的操作。如何在函数式编程中保持这样的联系?目前,我们只是将连接设为全局连接。

您的程序将具有状态。总是。您的程序将执行一些 I/O。几乎总是。函数式编程不是不做这些事情,而是控制它们:以一种合理限制使代码维护和推理能力复杂化的事情的方式来做它们。

至于您的特定功能,我认为它有一个问题:您将 创建连接 使用该连接做某事混为一谈.

你可能想从更像这样的东西开始:

const createConn = (arg) => createConnection(arg);
const doSomething = (conn, arg) => conn.doSomething(arg);

请注意,这更容易测试:您可以通过单元测试中的模拟,而您的原始测试无法通过这种方式。一个更好的方法是有一个缓存:

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

现在您的 getConn 函数是幂等的。更好的方法仍然是建立连接 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);
    });
  };
}

现在创建的细节被抽象掉了。请注意,这仍然是有状态的和混乱的,但现在 consuming 代码可以更实用并且更容易推理:

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

最后,当尝试实现功能时(尤其是在不基于该范式构建的语言中),您只能使用套接字做很多事情。或文件。或者来自外界的任何其他东西。所以不要:不要试图让一些本来就具有副作用的功能发挥作用。相反,将一个好的 API 作为一个抽象 并适当地分离您的关注点,以便您的代码的 rest 可以拥有所有功能代码的理想属性。