documentcloud / underscore-contrib

The brass buckles on Underscore's utility belt
MIT License
621 stars 117 forks source link

deepGet (deepAccessor?) #28

Closed joshuacc closed 11 years ago

joshuacc commented 11 years ago

I just wrote up an implementation of deepGet for a separate library and thought that it might fit in well with underscore-contrib's object selectors.

The goal is to avoid the typical hassle of dealing with TypeError: Cannot read property 'someprop' of undefined (and the similar error for null) when trying to access deeply nested properties. deepGet will transparently handle those cases for you.

In my implementation both null and undefined are treated as non-values.

Implementation: https://github.com/joshuacc/drabs/blob/master/src/drabs.js#L5

Usage:

var rels = {
    Viola: {
        Orsino: {
            Olivia: {
                Cesario: null
            }
        }
    }
};

// Getting a deep property that has a real value
deepGet(rels, ["Viola", "Orsino", "Olivia"]);
//=> { Cesario: null }

// Getting a non-existent property
deepGet(rels, ["Viola", "Harry", "Sally"]);
//=> undefined

// Getting a null property
deepGet(rels, ["Viola", "Orsino", "Olivia", "Cesario"]);
//=> undefined

// Getting a deep property via a string-based property list
deepGet(rels, "Viola.Orsino.Olivia");
//=> { Cesario: null }

// Returning a default value if the property was a non-value/non-existent
deepGet(rels, ["Viola", "Orsino", "Olivia", "Cesario"], "Hmmm.");
//=> "Hmmm."

It's not currently written in the same style as Underscore, but if there is interest, I can take a crack at adapting it.

puffnfresh commented 11 years ago

Functional lenses are a composable and better abstracted form of this. bilby.js has a limited form of them. The idea is:

https://github.com/pufuwozu/bilby.js/blob/master/test/lens.js

Lenses can be passed around to functions and composed together. You then run them over a target to retrieve/update a nested object. Even better: lenses don't just work on objects, they can work on arrays, streams, trees, etc - and still be composed together (e.g. get the tree from the 2nd index of array inside of object key 'a'). How cool is that?

michaelficarra commented 11 years ago

Lenses are sufficient but not necessary for this problem. We can easily solve it using the Maybe monad. You just need a little bit of a standard library with Maybe and Monad support first:

// data Maybe t = Nothing | Just t
function Nothing(){}
function Just(v){ this.value = v; }

function maybe(defaultValue, fn, m){ return m instanceof Nothing ? defaultValue : fn(m.value); }

// instance Monad Maybe where
function maybeUnit(x){ return new Just(x); }
function maybeBind(x, f) { return maybe(x, f, x); }

// the identity function
function id(x){ return x; }

Now you can easily implement this function yourself:

// this generates the access operation
function maybeAccessor(p){
  return function(o){ return o[p] == null ? new Nothing : maybeUnit(o[p]); };
}

function deepGet(o, props, defaultValue){
  var accessors = _.map(typeof props === 'string' ? props.split('.') : props, maybeAccessor);
  return maybe(defaultValue, id, _.foldl(accessors, maybeBind, maybeUnit(o)));
}

// now you have what you wanted
deepGet(rels, ["Viola", "Orsino", "Olivia"]);

edit: We could also have just folded the Kleisli composition over the accesses and applied the result to the input o:

return maybe(defaultValue, id, _.foldl(accessors, maybeKleisli, maybeUnit)(o));

edit again: The Haskell equivalents:

λ> let transforms = [(\x -> return $ x + 1), (\x -> return $ x + 2)] :: [(Int -> Maybe Int)]
λ> foldl (>>=) (return 0) transforms
Just 3
λ> foldl (>=>) return transforms 0
Just 3
joshuacc commented 11 years ago

@pufuwozu That looks quite cool. I'll try to dig into Bilby some to get a better handle on how that works.

@michaelficarra That looks like a much more elegant implementation than mine. :-)

I'm curious as to what contrib's philosophy is. Will it stick primarily to more basic functional utilities and leave it to users to compose things like deepGet? Or will some of those more specific utilities find a home in contrib?

Personally I see value in things like deepGet being per built and bundled into contrib, but can understand why you might not want to do that.

joshuacc commented 11 years ago

Ahem. "Pre-built."

fogus commented 11 years ago

I'm all for something like deepGet, in fact I have its dual in updatePath already in object.builders:

var a = [1, 2, {a: [{b: 42}]}, 3]
_.setPath(a, 100000, [2, 'a', 0, 'b'])
//=> [1, 2, {a: [{b: 100000}]}, 3]

It's not built on monadic machinery nor lenses, but it could be. The problem is that I've held off incorporating either yet and I've tried to avoid cross-contrib dependencies so that the library could be an a la carte affair. This condition may not hold forever, but I'd like to shoot for it in any case.

joshuacc commented 11 years ago

@fogus Awesome. I'll see about putting together a pull request today or tomorrow.