Let's say you need to connect to a database.
So, you give a DbConnection
as the last argument of some hypothetical function with a type like this: doDbStuff :: Int -> DbConnection -> Int
Perhaps there're other functions which also depend on a DbConnection
, and all of them are performing write operations. Thus, these might be ran separately or as part of an atomic operation (i.e. a transaction).
Since one may want to manage DbConnection
using a pool, and functions may or may not be part of an atomic operation, these doesn't implement code to acquire and release DbConnection
instances from and to the pool.
Now, these functions are part of a long function composition on which certain decisions may involve not requiring the DbConnection
. That is, there's a chance that a DbConnection
may be taken from the pool and it could be used by another request, which might produce bottlenecks.
There's an alternative, where one won't inject DbConnection
but a high-order function like withConnection :: (DbConnection -> a) -> a
, so each function can take a DbConnection
, use it and the whole withConnection
takes care of acquiring and releasing connections. The drawback here is that it's harder to make many functions to collaborate as part of an atomic operation.
For now, I've been using #2 approach. BTW, is there any alternative on which one could retain the best of both approaches?
Pseudo-code in JavaScript:
const connectionString = '[whatever]'
const env = { connection: acquire (connectionString) }
const output = composition (arg0) (argN) (env)
// then, release the connection
// f :: a -> b -> { connection: DbConnection }
const f = x => y => ({ connection }) =>
doDbStuff (x + y) (connection)
const withConnection = f => [stuff to acquire the connection, and aftewards, release it]
const env = { withConnection }
const output = composition (arg0) (argN) (env)
// type FnConnection DbConnection c = c -> a
// f :: a -> a -> { connection: FnConnection }
const f = x => y => ({ withConnection }) =>
withConnection (doDbStuff (x + y))
There is a tool for this situation and your solution is pretty close to it! The reader adt will allow you to write your functions within the context of having access to some environment. Here is my favorite implementation: https://github.com/monet/monet.js/blob/master/docs/READER.md
Unfortunately this pattern may require large amounts of your code to be wrapped in the reader type - but you are already introducing the withConnection wrapper which results in nearly the same amount of extra code.
Here is an example that reads a document with id of'123' from the db, overrides some properties, and writes the result back into the db. providing the db connection is deffered until actually running your program, but you can write your code assuming the db connection will be present when the code is run.
const { Reader } = require('monet');
const findById = (id) => Reader(({ db }) => db.find({ id }));
const insertDoc = (doc) => Reader(({ db }) => db.insert(doc));
const copyWithDefaults = (doc) => ({
...doc,
name: 'default name',
});
const app =
findById('123')
.map(copyWithDefaults)
.chain(insertDoc)
app.run({ db: aquire(connectionString) })
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With