Skip to content

Instantly share code, notes, and snippets.

@TimMensch
Last active November 8, 2018 23:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TimMensch/727407bdc522bc9900dae1786951c9e4 to your computer and use it in GitHub Desktop.
Save TimMensch/727407bdc522bc9900dae1786951c9e4 to your computer and use it in GitHub Desktop.
An alternative to the "Functional River"
// Easier to follow. Equally easy to test -- and with one fewer function in need of testing.
const Promise = require("bluebird");
const { hashStringAsync } = Promise.promisifyAll(require("./lib/crypto"));
const { logEventAsync } = Promise.promisifyAll(require("./lib/log"));
const { openAsync } = Promise.promisifyAll(require("./lib/db"));
const { TimeoutError, ValidationError, NotFoundError } = require("./errors");
const _openHandle = openAsync(); // FYI: Promises include memoization (caching) built into same API
module.exports = { auth };
/* auth is our main function */
async function auth({ username, password,
_onTimeoutError = errorHandler,
_onNotFoundError = errorHandler,
_onValidationError = errorHandler,
}) {
try {
// No more reusing "password" as "hashed password".
// Still starting the hash ASAP and not waiting for it.
const hashedPasswordPromise = hashStringAsync(password);
authValidate({ username, password });
const users = await usersModel();
const user = await users.findOneAsync({
username,
// I would have renamed "password" here; bad form to have a field named
// password that's expecting a hash.
password: await hashedPasswordPromise, // Deferred await to when we need it.
});
await userFound(user);
return userInfo;
} catch (e) {
// Left this in because Bluebird's extension for matching errors is cute.
Promise.reject(e)
.catch(TimeoutError, _onTimeoutError)
.catch(NotFoundError, _onNotFoundError)
.catch(ValidationError, _onValidationError)
.catch(errorHandler);
}
}
function authValidate({ username, password }) {
if (!username || username.length < 4) {
throw new ValidationError("Invalid username. Required, 4 char minimum.");
}
if (!password || password.length < 4) {
throw new ValidationError("Invalid password. Required, 4 char minimum.");
}
}
function userFound(results) {
if (results) return;
throw new NotFoundError("No users matched. Login failed");
}
function usersModel() {
return _openHandle.then(({ models }) => models.users);
}
function errorHandler(err) {
console.error("Failed auth!", err);
}
@justsml
Copy link

justsml commented Nov 8, 2018

I agree the interfaces I came up with were a little contrived - it's a balance I tried to strike while still being somewhat copy-pastable.

Too many articles talk about Functional Purity using un-relatable or super granular examples: Math.max.

I wanted to show an applied & recognizable pattern to most (backend) devs. This necessarily means I have to make trade-offs: so it's both digestible, and not overly simplified.

Figuring out how to scope your closures & pass variables between steps is (to me) a separate qualitative discussion.
Differing priorities will dictate how this gets decided. In researching this area, I've been working on an (unpublished) article for about a year detailing how this plays out in Haskell, Erlang, Elm, and F#. (Each have varying ways to 'pass-through' values, or scope values using Monads, etc.) I believe this will address much of your misgivings.


I'm a little confused when I hear things like the "...functional approach you're advocating is actually an anti-pattern."

You're not the first person to tell me that - and I'm trying to understand where this value-judgement is coming from.

Before I continue, let me mention that the word 'Functional' gets thrown around to mean many different things. This is unfortunate. (And I know I'm not helping with a library called Functional Promises) - I don't want to get bogged down on whether I mean strictly Functional Programming ala Computer Science Purists or referring to Lambda calculus designs of math purists... This is a debate for another day. For now, let's unpack the "functional/anti-pattern" thing...

Anti-patterns in code are rarely absolutes. Everything is about trade-offs.

So, trying to figure out exactly you're talking about:

  • If you were referring to my scoping decisions - I'm sure they could be improved. PRs welcome. 😺
  • If however you are talking about function purity: parameters & return values... I must respectfully disagree. 😇

In Eric Elliot's article he alludes to the importance of return values (and that they ought to be useful) here:

key aspect of pure functions

It took me a long time to realize the benefit of this. Mostly because I was raised in an OOP Java/C# world: I used to think it was perfectly fine to return true for isValid() style functions. After many years, I see how brittle & opaque this approach is.

To recap: the most important part to me is prioritizing Pure (& versatile) functions.

Of course virtually no app is 100% pure functions, at some point you need to persist data or get data over a network; the key is when that happens it's tightly scoped w/ Single Purpose in mind. Ultimately any transformations, validation, or security checks should be alongside your side-effect functions - helps you avoid tightly coupled logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment