Deep picking

Grab a subset of a complex object using functional-extract utility.

Imagine we have a complex object and would like to extract a few properties. For example, we would like to print first name and age from the following object

1
2
3
4
5
var joe = {
first: 'joe',
last: 'smith',
age: 20
};

We could use the functional version of the popular lodash library that switches the order of arguments to something more sensible in my opinion.

1
2
3
var _ = require('lodash-fp');
console.log(_.pick(['first', 'age'], joe));
// { first: 'joe', age: 20 }

What if the input object has deep nesting and we would like to pick properties deep inside it? Imaging that the first name we need is nested inside an object

1
2
3
4
5
6
7
var joe = {
name: {
first: 'joe',
last: 'smith'
},
age: 20
};

We can try using . syntax in the property name, but that does not work with _.pick yet

1
2
console.log(_.pick(['name.first', 'age'], joe));
// { age: 20 }

Remember, our goal is to produce a simple object with simple property names

1
2
3
// fn is our "pick" function
console.log(fn(joe));
// { name: 'joe', age: 20 }

Extracting single deep property

Let us try a different approach. We can extract a single deep property using a feature introduced in the [email protected] version

1
2
console.log(_.get(joe, 'name.first'));
// joe

Except this method is not part of the lodash-fp and requires the object to be specified first. Another function that works very well in this situation is functional-pipeline. One can extract a deep property quickly

1
2
3
var fp = require('functional-pipeline');
fp('name', 'first')(joe);
// 'joe'

Extracting multiple properties

Let us tackle the problem of extracting multiple deep properties using functional-pipeline. We can extract each property by applying the object at the end, but this forces the object to exist when we declare the extraction.

1
2
3
4
5
6
var simple = {
name: fp('name', 'first')(joe),
age: fp('age')(joe)
};
console.log(simple);
// { name: 'joe', age: 20 }

This is good, but I really would like to declare the extraction mapping and then apply it to any object. Let us NOT apply the function returned from fp('age') to the object joe, instead leaving it "free".

1
2
3
4
5
6
var simple = {
name: fp('name', 'first'),
age: fp('age')
};
// simple is an object
// { name: [Function], age: [Function] }

The object simple is temporary: its properties are not the final values, but the extractor functions like name: [Function]. Each extractor function needs be called with an actual object; only then it will return the extracted value. The extractor functions can also be reused.

We need to write a new function that will go through each extractor property and actually fill it with values.

1
2
3
4
5
function extract(schema, obj) { ... }
console.log(extract(simple, joe));
// { name: 'joe', age: 20 }
console.log(extract(simple, ann));
// { name: 'ann', age: 10 }

We can write the extract function ourselves: it is very small.

1
2
3
4
5
6
7
8
9
10
11
12
function extract(picks, from) {
var result = {};
Object.keys(picks).forEach(function (key) {
if (typeof picks[key] === 'function') {
// assume we have an extractor function
result[key] = picks[key](from);
} else {
result[key] = picks[key]; // A
}
});
return result;
}

I added the else branch that copies the value in line // A to allow adding new values.

1
2
3
4
5
6
7
var simple = {
name: fp('name', 'first'),
age: fp('age'),
gender: 'male'
};
console.log(extract(simple, joe));
// { name: 'joe', age: 20, gender: 'male' }

Working with collections

We can partially apply extract and use it to extract the deep properties over an array of objects

1
2
3
4
var toSimple = extract.bind(null, simple);
var people = [joe, ann];
console.log(people.map(toSimple));
// [{ name: 'joe', age: 20 }, { name: 'ann', age: 10 }]

Compatible with other methods

You are not limited to the functional-pipeline method for getting the nested values. You can use other libraries to extract a property from an object, for example lodash/get, as long as it returns a function that will receive the actual object reference to work with.

1
2
3
4
5
6
7
8
9
10
11
12
function _get(path) {
return function (obj) {
return _.get(obj, path);
};
}
var simple = {
name: _get('name.first'),
age: _get('age')
};
var result = fe(simple, joe);
// result is
// { name: 'joe', age: 20 }

Conclusion

I placed the extract code into its own tested repo bahmutov/functional-extract and made it available on NPM and bower under name functional-extract. Please use it and let me know if there are features or bugs.

Related

  • l33teral. This almost does what I need, but one cannot simplify the extracted keys. Instead the path values are used, for example
1
2
3
var leet = require('l33teral');
leet(joe).extract('name.first', 'age');
// { 'name.first': 'joe', age: 20 }

I really did not want the keys to be the paths. The l33teral also puts the object first, making it difficult to work with collections.