Yomguithereal / baobab

JavaScript & TypeScript persistent and optionally immutable data tree with cursors.
MIT License
3.15k stars 117 forks source link

Flat representation through embedded arrays #440

Open aalbul opened 8 years ago

aalbul commented 8 years ago

Oh i spend so much time thinking about the title for this issue. Let me show you an example so it will explain it better. Let's assume that i have a tree like this:

const tree = new Baobab({
    hosts: [{
        name: 'one',
        deployeds: [{
            name: 'one/deployed1',
            selected: true
        }]
    }, {
        name: 'two',
        deployeds: [{
            name: 'one/deployed2',
            selected: false
        }]
    }, {
        name: 'three',
        deployeds: [{
            name: 'one/deployed3',
            selected: false
        }]
    }]
});

Now, i would like to select a list of all deployed names. I'm trying to do something like:

console.log(tree.select('hosts', 'deployeds', 'name').get());

But, off course getting "undeffined". The reason, i suppose, is that selector tries to get a field 'deployeds' from of 'hosts' array field and, off course it fails.

Is there are any way to do the query like that? (In an elegant way :))

Yomguithereal commented 8 years ago

Hello @aalbul. To do so, you can either rely on raw JavaScript array methods such as .map and .reduce and such but since you will probably need to flatten arrays, you should consider using libraries such as lodash to do so.

Using raw JavaScript

var names = tree.get('hosts')

  // Here we recursively retrieve the name of each deployed
  // This will produce something like the following:
  // [['name1', 'name2'], ['name3', 'name4']]
  .map(function(host) {
    return host.deployeds.map(function(deployed) {
      return deployed.name
    });
  })

  // So now we need to flatten the result to obtain:
  // ['name1', 'name2', 'name3', 'name4']
  .reduce(function(acc, names) {
    return acc.concat(names);
  }, []);

Using lodash

var names = _(tree.get('hosts'))
  .map(function(host) {
    return _.map(host, 'name');
  })
  .flatten()
  .value();

Also know that if this is something you might want to do often in your code, you can also consider creating a monkey that will hold this computed data for you.

aalbul commented 8 years ago

Hi @Yomguithereal and thank you for your answer. For sure that's possible to do with the help of Lodash or just in raw JavaScript. The problem is that i was trying to achieve that with the help of Baobab to leverage it's caching support. The idea was to reduce the amount of intermediate result calculation to minimum. For example, this is how i did it with the help of "Monkey API":

const Baobab = require('baobab');
const _ = require('lodash');
const monkey = Baobab.monkey;

const tree = new Baobab({
    hosts: [{
        name: 'one',
        deployeds: [{
            name: 'one/deployed1',
            selected: true
        }]
    }, {
        name: 'two',
        deployeds: [{
            name: 'one/deployed2',
            selected: false
        }]
    }, {
        name: 'three',
        deployeds: [{
            name: 'one/deployed3',
            selected: false
        }]
    }]
});

tree.set('selectedDeployeds', monkey({
    cursors: {
        hosts: ['hosts']
    },
    get: (data) => {
        console.log('Calculating selected for all hosts.');
        return _.flatten(_.pluck(data.hosts, 'selectedDeployeds'));
    }
}));

tree.select('hosts').map((host) => {
    host.set('selectedDeployeds', monkey({
        cursors: {
            deployeds: _.flatten([host.solvedPath, 'deployeds'])
        },
        get: (data) => {
            console.log('Calculating selected for host', data);
            return _.filter(data.deployeds, (deployed) => deployed.selected === true);
        }
    }))
});

console.log(tree.get('selectedDeployeds'));
tree.select('hosts', 2, 'deployeds', 0).set('selected', true);
console.log(tree.get('selectedDeployeds'));

The output will look like:

/usr/local/bin/node events.js
Calculating selected for all hosts.
Calculating selected for host { deployeds: [ { name: 'one/deployed1', selected: true } ] }
Calculating selected for host { deployeds: [ { name: 'one/deployed2', selected: false } ] }
Calculating selected for host { deployeds: [ { name: 'one/deployed3', selected: false } ] }
[ { name: 'one/deployed1', selected: true } ]
Calculating selected for all hosts.
Calculating selected for host { deployeds: [ { name: 'one/deployed3', selected: true } ] }
[ { name: 'one/deployed1', selected: true },
  { name: 'one/deployed3', selected: true } ]

Process finished with exit code 0

The idea is to settle monkeys on every "host" node so it can cache the results within it and then to settle a monkey within a global object so it will cache results of the host's monkeys. This means that we can reduce the amount of iterations significantly.

And as you see, It works. However i'm not really happy with implementation: 1) It's not fluent. (as for me) 2) I have to manage monkey instances on each of "host" all the time. I mean, when i'm adding "host" to tree, i need to register a monkey for it. Do i need to un-register it BTW? Or it will be garbage collected? 3) I would better prefer to get a cursor to it and use the caching. It will allow you to make an ad-hock queries instead of creating a separate monkey for every specific case. I see that current implementation does not allow to use cursors to traverse objects inside of arrays. This is something that done intentionally or just not implemented yet?