functional javascript w/ composition

node v6.17.1
version: 9.0.3
endpointsharetweet
One of the most important ideas in software development is: keep it simple. The problem is, every developer believes all they write is simple (& beautiful) code. ... I'm going to avoid wading into the tarpit of defining "Simple Code." Instead I'll show 2 'rules' which have helped me write more testable & adaptable code. (Which I feel are key to Simple code) 1. Restrict functions to single-purpose. (Single Responsibility Principle) 2. Restrict functions to single-argument (or 2 for (err, value) style). This can be an array or object with many-faceted data. You may be wondering how single-purpose functions ever amount to anything useful, well, let me introduce you to my friend, Higher Order Components, or HOCs. HOC is really a fancy way of saying Function. You may know HOCs by other names: The elaborate excuse for what I used to call my "Controllers," are now discreet wrappers of function chains. Let's do a few functions with simple math. Using Pure ES2016 - and a tape test. Includes examples w/ traditional functions, built-ins (Array.reduce), and promises.
/* @author: Dan Levy <Dan@DanLevy.net> Twittering: @justsml */ const test = require('tape'); // The '_composer' wrapper fn supports reversing execution order, literally only changes the inner fn '_partial' const compose = _composer(); // note: '_composer' defined at the end, read tests first const composeRight = _composer(true); // Define a Pure-at-heart function (https://en.wikipedia.org/wiki/Pure_function) const add5 = n => { n = parseInt(n) + 5 return n; // required } test('sequence of 3 functions', t => { const add15 = compose(add5, add5, add5); t.equals(add15(0), 15) t.equals(add15(5), 20) t.equals(add15('5'), 20) t.end(); }) // Define some Pure-at-heart functions (https://en.wikipedia.org/wiki/Pure_function) const half = n => { n = (parseInt(n) * 0.5).toFixed(2) return n; // required } const square = n => { n = (parseInt(n * n)).toFixed(2) return n; // required } /* Code reuse & independence across varying patterns #AllTheMonads */ test('composition: math functions', t => { const add5HalfSquare = compose(add5, half, square); t.equals(add5HalfSquare(5), '25.00', 'I can caz maths?'); // 5+5==10, half(10)==5, 5*5==25.0 t.end(); }) test('promises: math functions', t => { const add5HalfSquare = n => Promise.resolve(n) .then(add5).then(half).then(square); add5HalfSquare(5) .then(answer => { t.equals(answer, '25.00', 'I can promise maths?'); // 5+5==10, half(10)==5, 5*5==25.0 t.end(); }); }) test('array.reduce: math functions', t => { const add5HalfSquare = n => [add5, half, square] .reduce((val, fn) => fn && fn(val), n); t.equals(add5HalfSquare(5), '25.00', 'I can reduce maths?'); // 5+5==10, half(10)==5, 5*5==25.0 t.end(); }) // Plz ignore any 'fanciness' here - the point is the pattern above: // Forward/reverse function chain helper - tldr: this glues other functions toghether function _composer(reverse = false) { // @uglytruth // Returns higher-order-function, executes using Array.reduce syntax (link mdn article). // Starting with the initial arguments, each function is sequentially executed, passing arguments from one function to the next - functions MUST RETURN THE VALUE TO HAND-OFF. // Only supports single param functions - **this is a feature** return function _directionalFunChain() { const fns = Array.from(arguments); return function _partial() { return Array.from(fns) [reverse ? 'reduceRight' : 'reduce']((last, fn) => fn && fn(last) || last, [...arguments]); }.bind(this); } }
You may be thinking it's easier to I try organize clear 'pathways' in my code - as an example (forgive the psuedo-code) - I'm going to go through a login function for a chat app. Requirements: 1. User clicks login. 2. Modal prompts for `user` and `pass` fields. 2a. User submits form. 2b. User clicks 'forgot password'. 3. Upon success, load app+msg data for logged-in user. 4. Upon failure, plunge into fire pit. 5. Show appropriate messaging (modal/toast). We just started Or a chat app's login/auth path: chatApp.login = () => openLoginModal() .then({user, pass}) => ajaxLogin({user, pass})) .then(user => { this.setUserStatus('active') // < non blocking, as result is irrelevant. // The next line calls 3 async functions, when they resolve, on to the next .then() return [getContacts(user), getRooms(user), getDirectMessages(user)] }) .then(([contacts, rooms, dms]) => { this.alertOnNew({contacts, dms}) return this.cache({contacts, rooms, dms}) }) .catch(UserCancel, ========
Loading…

2 comments

  • posted 7 years ago by brianleroux
    <3 it!
  • posted 7 years ago by thevige
    fantastic

sign in to comment