Friday, March 6, 2015

Planting values in lodash wrappers

Wrapping values in lodash let's us compose chains of functionality. There's two reasons to do this. The first is that the resulting code is a lot more compact and readable, especially if each chained call is on it's own line. The second reason, now that lazy evaluation is supported in lodash, is efficiency. Some chains won't iterate over the entire collection if it doesn't have to. Now there's a third reason — the plant() function. This function was made available after Lo-Dash Essentials was published, so I'll talk about it here.

As our lodash code grows, we'll start to notice lot's of wrappers with chained function calls. Many of which are similar. Before plant(), wrappers weren't reusable with other values. With the introduction of plant(), we can start reusing our wrappers in interesting ways. Let's make a generic function that plants new values into wrappers.

function plantValue(wrapper, value) {
    if (_.isUndefined(value)) {
        return wrapper;   
    } else {
        return wrapper.plant(value);    
    }    
}

The plantValue() function takes two arguments. The first is a lodash wrapper. The second is an optional value, if provided, it is planted into the wrapper. This actually creates a new wrapper, so there's no side-effects on the wrapper argument. Let's make some functions that use plantValue() now.

function odds(wrapper, value) {
    return plantValue(wrapper.filter(function(item) {
       return item % 2; 
    }), value);
}

function evens(wrapper, value) {
    return plantValue(wrapper.reject(function(item) {
        return item % 2;
    }), value);
}

The odds() and the evens() functions take the same arguments as plantValue(). The first is a lodash wrapper, the second being an optional value. We're using filter() and reject() in these functions to get the odd and even numbers from the wrapper, respectively. The value is just passed through to plantValue(). This means that, for example, we can call evens() with a wrapper, or a wrapper and a value. And this is where the reusable wrapper comes in handy. Let's see how this works.

var collection = _.shuffle(_.range(100));

var limitWrapper = _([])
    .sortBy()
    .dropRightWhile(function(item) {
        return item > 50;
    });

odds(limitWrapper, collection).value();
// → [1, 3, 5, 7, 9, 11, 13, 15, 17, 19,
//    21, 23, 25, 27, 29, 31, 33, 35, 37,
//    39, 41, 43, 45, 47, 49]

evens(limitWrapper, collection).value();
// → [0, 2, 4, 6, 8, 10, 12, 14, 16, 18,
//    20, 22, 24, 26, 28, 30, 32, 34, 36,
//    38, 40, 42, 44, 46, 48, 50]

The limitWrapper is a reusable wrapper we want to apply in several contexts. It sorts the wrapped collection, and takes anything that's under 50. We've passed this wrapper to our odds() and evens() invocations. In the results, we can see that the arrays are filtered accordingly, but they're also sorted, and don't contain anything larger than 50. The limitWrapper was initially wrapped with an empty array, because we don't have any intention of ever calling value() on limitWrapper directly. Instead, we want to copy the chained function calls — sortBy() and dropRightWhile() — into a new wrapper, with a new value.

Let's see if we can use plantValue() with functions that aren't chainable, like reduce().

function sum(wrapper, value) {
    return plantValue(wrapper, value)
        .reduce(function(result, item) {
            return result + item;
        });
}
Once again, the arguments expected by sum() are a wrapper, followed by an optional value to plant. What's done differently here is that we're calling plantValue() and then calling reduce on the returned wrapper. This is so that our sum() function can return an unwrapped value. The odds() and evens() functions, on the other hand, return wrappers themselves. Which means that we can use these functions to create reusable wrappers.
var oddsWrapper = odds(limitWrapper);
var evensWrapper = evens(limitWrapper);

sum(oddsWrapper, collection);
// → 625

sum(evensWrapper, collection);
// → 650

Now we have two more reusable wrappers — oddsWrapper and evensWrapper. These were created by their respective odds() and evens() functions. They each have the limitWrapper embedded as well, because that's what odds() and evens() were called with as their first arguments. If we didn't want this limiting behavior in the oddsWrapper, for instance, we could just pass an empty wrapper instead — _([]). The sum() function can now use the resuable oddsWrapper and evensWrapper instances, planting new values when called.