Turning simple Node.JS Postgres query and logging into most-functionally-pure-possible code

Posted on

Problem

After reading “How to deal with dirty side effects in your pure functional JavaScript,” I’ve decided to take something I do on a regular basis (“Original approach” below)—connect to a Postgres database from Node.JS, perform some operations, and log what is going on—and try to make my code much closer to “purely functional” (“Functional approach” below) by using “Effect functors.”

Am I on the right track?

Beyond functional-vs-declarative, are there other ways my code could be better-written?


Original approach – declarative

const {user, database, host} = require('./mydatabaseconnection') ;
const pg = require('pg') ;
const logging = require('./logging') ;
const pglogging = require('./pglogging') ;

// MODIFIED FROM https://medium.com/@garychambers108/better-logging-in-node-js-b3cc6fd0dafd
["log", "warn", "error"].forEach(method => {
    const oldMethod = console[method].bind(console) ;
    console[method] = logging.createConsoleMethodOverride({oldMethod, console}) ;
}) ;

const pool = new pg.Pool({
    user
    , database
    , host
    , log: pglogging.createPgLogger()
}) ;

pool.query('SELECT 1 ;').then(result => console.log(result)).then(()=>pool.end()) ;

Functional approach

const {user, database, host} = require('./mydatabaseconnection') ;
const pg = require('pg') ;
const logging = require('./logging') ;
const pglogging = require('./pglogging') ;

// MODIFIED FROM https://medium.com/@garychambers108/better-logging-in-node-js-b3cc6fd0dafd
["log", "warn", "error"].forEach(method => {
    const oldMethod = console[method].bind(console) ;
    console[method] = logging.createConsoleMethodOverride({oldMethod, console}) ;
}) ;

function Effect(f) {
    return {
        map(g) {
            return Effect(x => g(f(x)));
        }
        , runEffects(x) {
            return f(x) ;
        }
    }
}

function makePool() {
    return {pool: new pg.Pool({
        user
        , database
        , host
        , log: pglogging.createPgLogger()
    })} ;
}

function runQuery({pool}) {
    return {query: pool.query('SELECT 1 ;'), pool} ;
}

function logResult({query, pool}) {
    return {result: query.then(result => console.log(result)), pool} ;
}

function closePool({pool}) {
    return pool.end() ;
}

const poolEffect = Effect(makePool) ;
const select1Logger = poolEffect.map(runQuery).map(logResult).map(closePool) ;

select1Logger.runEffects() ;

Common to both

logging.js

const moment = require('moment-timezone') ;

function simpleLogLine({source = '', message = '', objectToLog}) {
    const objectToStringify = {date: moment().toISOString(true), source, message} ;
    if (typeof objectToLog !== 'undefined') objectToStringify.objectToLog = objectToLog ;
    return JSON.stringify(objectToStringify) ;
}

module.exports = {
    createConsoleMethodOverride: ({oldMethod, console}) => function() {
        oldMethod.apply(
            console,
            [(
                arguments.length > 0
                ? (
                    (
                        arguments.length === 1
                        && typeof arguments[0] === 'object'
                        && arguments[0] !== null
                    )
                    ? (
                        ['message', 'objectToLog'].reduce((accumulator, current) => accumulator || Object.keys(arguments[0]).indexOf(current) !== -1, false)
                        ? simpleLogLine(arguments[0])
                        : simpleLogLine({objectToLog: arguments})
                    )
                    : (
                        arguments.length === 1 && typeof arguments[0] === 'string'
                        ? simpleLogLine({message: arguments[0]})
                        : simpleLogLine({objectToLog: arguments})
                    )
                )
                : simpleLogLine({})
            )]
        ) ;
    }
} ;

pglogging.js

module.exports = {
    createPgLogger: () => function() {
        console[
            typeof arguments[0] !== 'string'
            ? 'log'
            : (
                arguments[0].match(/error/i) === null
                ? 'log'
                : 'error'
            )
        ](
            arguments.length === 1
            ? (
                typeof arguments[0] !== 'string'
                ? ({source: 'Postgres pool', object: arguments[0]})
                : ({source: 'Postgres pool', message: arguments[0]})
            )
            : (
                typeof arguments[0] !== 'string'
                ? ({source: 'Postgres pool', object: arguments[0]})
                : (
                    arguments.length === 2
                    ? ({source: 'Postgres pool', message: arguments[0], object: arguments[1]})
                    : ({source: 'Postgres pool', message: arguments[0], object: [...arguments].slice(1)})
                )
            )
        ) ; 
    }
} ;

Sample output (from either approach):

{"date":"2019-02-23T12:31:02.186-05:00","source":"Postgres pool","message":"checking client timeout"}
{"date":"2019-02-23T12:31:02.193-05:00","source":"Postgres pool","message":"connecting new client"}
{"date":"2019-02-23T12:31:02.195-05:00","source":"Postgres pool","message":"ending"}
{"date":"2019-02-23T12:31:02.196-05:00","source":"Postgres pool","message":"pulse queue"}
{"date":"2019-02-23T12:31:02.196-05:00","source":"Postgres pool","message":"pulse queue on ending"}
{"date":"2019-02-23T12:31:02.203-05:00","source":"Postgres pool","message":"new client connected"}
{"date":"2019-02-23T12:31:02.203-05:00","source":"Postgres pool","message":"dispatching query"}
{"date":"2019-02-23T12:31:02.207-05:00","source":"Postgres pool","message":"query dispatched"}
{"date":"2019-02-23T12:31:02.208-05:00","source":"Postgres pool","message":"pulse queue"}
{"date":"2019-02-23T12:31:02.209-05:00","source":"Postgres pool","message":"pulse queue on ending"}
{"date":"2019-02-23T12:31:02.209-05:00","source":"","message":"","objectToLog":{"0":{"command":"SELECT","rowCount":1,"oid":null,"rows":[{"?column?":1}],"fields":[{"name":"?column?","tableID":0,"columnID":0,"dataTypeID":23,"dataTypeSize":4,"dataTypeModifier":-1,"format":"text"}],"_parsers":[null],"RowCtor":null,"rowAsArray":false}}}

Solution

Not at all functional.

  1. Functional means no side effects.

    You have console[method], new pg.Pool and pool.query each of which are side effects.

  2. Functional means using pure functions.

    • A pure function must be able to do its thing with only its arguments.

      logResult and makePool require global references.

    • A pure function must always do the same thing for the same input.

      runQuery, closePool, and makePool depend on the database state not the arguments for the result and are not pure.

      Because you pass impure functions to Effect you can not predict its behavior and thus it is impure as well.

Now you may wonder how to fix these problems and make your code functional.

The best way to picture the problem is to imagine that each function must run on a isolated thread and rely only on its arguments to create a result. The arguments must be transportable and not rely on an external state (Remember the function is totally isolated)

If you break these rules even once in a mile of code you break the tenet that must be maintained to get the benefit that functional programming offers.

There is no half way functional, it’s all or nothing.

Leave a Reply

Your email address will not be published. Required fields are marked *