1
\$\begingroup\$

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

\$\endgroup\$
  • \$\begingroup\$ From the answer I've received so far, it appears I've still got a long ways to go to really grok the pure-functional paradigm. Can anyone recommend any tutorial, further reading, etc.? For instance: glebbahmutov.com/blog/node-server-with-rx-and-cycle/… - "…moving of everything that is outside of our control to inputs or outputs, leaving our main logic inside a pure function". Do I need to do something like this? \$\endgroup\$ – rvrabbit Mar 2 at 19:16
1
\$\begingroup\$

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 tenant that must be maintained to get the benefit that functional programming offers.

There is no half way functional, its all or nothing.

\$\endgroup\$
  • \$\begingroup\$ Thank you for the answer. I will take your advice and reexamine every function with this in mind: "imagine that each function must run on a isolated thread and rely only on its arguments to create a result." \$\endgroup\$ – rvrabbit Mar 2 at 16:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.