Skip to content

Instantly share code, notes, and snippets.

@karlrwjohnson
Last active February 27, 2018 16:17
Show Gist options
  • Save karlrwjohnson/5358f410bed8498bdc50c9d731e04442 to your computer and use it in GitHub Desktop.
Save karlrwjohnson/5358f410bed8498bdc50c9d731e04442 to your computer and use it in GitHub Desktop.
Lodash FP Proposal

Lodash FP Proposal

Premises

  • Immutable: All functions return a copy of the data instead of modifying it. Even functions which normally mutate data, like _set, are wrapped to make a copy first.

  • Auto-curried, Data-last: This one's harder to explain and the value isn't obvious at first.

    • Curried functions take their arguments one at a time, returning a callback function until it receives the last parameter. E.g. a curried version of f(a, b, c) is called as f(a)(b)(c).
    • Auto-curried functions can be called the normal way, but they produce curried versions of themselves until all their arguments are supplied. E.g. If f normally takes three arguments, you can call it as f(a,b,c), f(a,b)(c), f(a)(b)(c) and so on.
    • "Data-last" simply means they've rearranged the arguments for every Lodash function so the data parameter is the last one instead of the first.
    • The result of this is, it enables a programming style where you compose multiple Lodash FP functions into a super-function, and then pass the state as the very last argument. This will make more sense later.

Trivial example

Consider a reducer where you simply need to set a value:

createReducer({
    // State of a fictional running application

    foos: {
        123: {
            loadingState: NOT_STARTED,
            results: {
                id: 123,
                bar_id: 456,
            },
        }
    },

    editorState: {
        id: 123,
        bar_id: 456,
    },
}, {
    // Event handlers

    FOO_EDITOR_SET_BAR: (state, { barId }) => {
        // We can do this multiple ways:

        // Strategy 0 (WRONG): Mutate the state
        state.editorState.bar_id = barId;
        return state;

        // Strategy 1: Pyramid of exploded objects
        return {
            ...state,
            editorState: {
                ...state.editorState,
                bar_id: barId,
            }
        }

        // Strategy 2: Lodash to clone and set property
        const newState = _cloneDeep(state);
        _set(newState, 'editorState.bar_id', barId);
        return state;
    },
});

Strategy 1 does not scale well, we decided long ago.. Strategy 2 works, but for large deep-cloning a large piece of the state tree can cause many components to re-render unnecessarily, which slows down the app.

Lodash FP wraps data-mutating functions like _set() so they clone the original data first, but unlike _cloneDeep() they only clone the parts of the data structure that need to change.

import fpSet from 'lodash/fp/set';

const a = { foo: 'a', bar: [1,2,3] };
const b = fpSet('foo', 'b', a);

console.log(a); // {foo: "a", bar: [1, 2, 3]}
console.log(b); // {foo: "b", bar: [1, 2, 3]}

// Since *.bar was unaltered, `a` and `b` reference the same array, so React would assume it's unchanged.
console.log(a.bar === b.bar); // true
a.bar.push(4);
console.log(b); // {foo: "b", bar: [1, 2, 3, 4]}

You may notice this does the same thing as our deepAssign(). But unlike deepAssign someone else maintains and tests this code.

More complex example

import mapValues from 'lodash/mapValues';
import update from 'lodash/update';
import without from 'lodash/without';

// Non-Functional Programming solution
return {
    ...state,
    results: _mapValues(state.results, (organization) => {
        return {
            ...organization,
            customerListIds: _without(organization.customerListIds, [customerListId]),
        };
    }),
};

import fpMapValues from 'lodash/fp/mapValues';
import fpUpdate from 'lodash/fp/update';
import fpWithout from 'lodash/fp/without';

// Use with Lodash FP functions.
// Same as regular Lodash, but the `data` parameter is LAST instead of FIRST
return fpUpdate(
    'results',
    results => fpMapValues(
        organization => fpUpdate(
            'customerListIds',
            customerListIds => fpWithout(customerListId, customerListIds),
            organization
        ),
        results
    ),
    state
);

// Lodash FP automatically *curries* -- meaning, if you don't give all the parameters,
// instead of erroring it returns a copy of itself with the arguments pre-bound.
// So instead of doing `f(a, b)` you can also do `f(a)(b)`.
return fpUpdate(
    'results',
    results => fpMapValues(
        organization => fpUpdate(
            'customerListIds',
            customerListIds => fpWithout(customerListId)(customerListIds)
        )(organization),
    )(results)
)(state);

// Lambdas can be removed by substituting `x => f(x)` with `f`
return fpUpdate(
    'results',
    fpMapValues(
        // Remove the deleted customer list id from each org
        fpUpdate(
            'customerListIds',
            fpWithout(customerListId)
        )
    )
)(state);

// Let's collapse this a bit to see what it looks like.
return fpUpdate('results', fpMapValues(fpUpdate('customerListIds', fpWithout(customerListId))))(state);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment