Closed joshuacc closed 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?
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
@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.
Ahem. "Pre-built."
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.
@fogus Awesome. I'll see about putting together a pull request today or tomorrow.
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 fornull
) when trying to access deeply nested properties.deepGet
will transparently handle those cases for you.In my implementation both
null
andundefined
are treated as non-values.Implementation: https://github.com/joshuacc/drabs/blob/master/src/drabs.js#L5
Usage:
It's not currently written in the same style as Underscore, but if there is interest, I can take a crack at adapting it.