-1

I am learning functional programming in Javascript and using Ramda. I have this object

    var fieldvalues = { name: "hello there", mobile: "1234", 
                  meta: {status: "new"}, 
                  comments: [ {user: "john", comment: "hi"}, 
                           {user:"ram", comment: "hello"}]
                };

to be converted like this:

 {
  comments.0.comment: "hi", 
  comments.0.user: "john",
  comments.1.comment: "hello",
  comments.1.user: "ram",
  meta.status: "new",
  mobile: "1234",
  name: "hello there"
  }

I have tried this Ramda source, which works.

var _toDotted = function(acc, obj) {
  var key = obj[0], val = obj[1];

  if(typeof(val) != "object") {  // Matching name, mobile etc
    acc[key] = val;
    return acc;
  }

  if(!Array.isArray(val)) {     // Matching meta
    for(var k in val)
      acc[key + "." + k] = val[k];
    return acc;
  }

  // Matching comments
  for(var idx in val) {
    for(var k2 in val[idx]) {
      acc[key + "." + idx + "." + k2] = val[idx][k2];
    }
  }
  return acc;
};

// var toDotted = R.pipe(R.toPairs, R.reduce(_toDotted, {}));
var toDotted = R.pipe(R.toPairs, R.curry( function(obj) {
  return R.reduce(_toDotted, {}, obj);
}));
console.log(toDotted(fieldvalues));

However, I am not sure if this is close to Functional programming methods. It just seems to be wrapped around some functional code.

Any ideas or pointers, where I can make this more functional way of writing this code.

The code snippet available here.

UPDATE 1

Updated the code to solve a problem, where the old data was getting tagged along.

Thanks

rsmoorthy
  • 2,284
  • 1
  • 24
  • 27
  • The actual problem with your code is that `toDotted` does always return the same object, regardless how you call it. Also it does not recurse into the object, so it works only for your particular example. – Bergi Jul 30 '16 at 15:28
  • @Bergi I am not quite sure if I understood what you said. I have commented the code, indicating some thing specific to this -- just for understanding. This code works for any json object, with nesting one level deeper (which is sufficient for my use case) and any given set of key-value combination. – rsmoorthy Jul 30 '16 at 15:37
  • The first problem is that `toDotted(a) === toDotted(b)`, the result will contain properties from both arguments. The second problem is that it *only* works for objects nested at most two levels, and even then only if the inner is either a primitive, a plain object or an array of objects (and nothing else). And actually a third problem is [using `for…in` enumerations on arrays](https://stackoverflow.com/q/500504/1048572). – Bergi Jul 30 '16 at 15:46
  • @Bergi 1) Thanks for pointing. I fixed that problem (not really the result was equal, but it was containing properties from both args 2) Yes, that's correct. For now, this is okay for me. 3) Yes, for...in being used, but this is just illustrative code. I am more concerned about doing it in FP way, so we can ignore these aspects. – rsmoorthy Jul 30 '16 at 16:17
  • @rsmoorthy what is the purpose of doing this ? Is it just an exercise or does it have a practical purpose ? – Mulan Jul 30 '16 at 18:53
  • @naomik This example is from a real example in my app. The input data can come in the nested or dotted format (one of the inputs and output described above). The MongoDB database accepts data in nested form (the input in this exercise) for add, while it accepts in modified dotted form for edit and also in conditions passed. Hence the need to interchange from these two formats is regular. However, here the focus is: how to do it in FP way. – rsmoorthy Jul 30 '16 at 19:02
  • Why is this down-voted? – rsmoorthy Aug 01 '16 at 15:39

2 Answers2

3

A functional approach would

  • use recursion to deal with arbitrarily shaped data
  • use multiple tiny functions as building blocks
  • use pattern matching on the data to choose the computation on a case-by-case basis

Whether you pass through a mutable object as an accumulator (for performance) or copy properties around (for purity) doesn't really matter, as long as the end result (on your public API) is immutable. Actually there's a nice third way that you already used: association lists (key-value pairs), which will simplify dealing with the object structure in Ramda.

const primitive = (keys, val) => [R.pair(keys.join("."), val)];
const array = (keys, arr) => R.addIndex(R.chain)((v, i) => dot(R.append(keys, i), v), arr);
const object = (keys, obj) => R.chain(([v, k]) => dot(R.append(keys, k), v), R.toPairs(obj));
const dot = (keys, val) => 
    (Object(val) !== val
      ? primitive
      : Array.isArray(val)
        ? array
        : object
    )(keys, val);
const toDotted = x => R.fromPairs(dot([], x))

Alternatively to concatenating the keys and passing them as arguments, you can also map R.prepend(key) over the result of each dot call.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    Thank you very much. Exactly what I was looking for. It contains all the aspects that I missed and you stressed.. breaking into small problems, recursion and the thing that I was struggling with -- the conditional scenarios. More importantly, this gives me ideas of how to attack other scenarios that I am facing. I will be spending more time with your answer and internalize / improvise (if possible). – rsmoorthy Aug 01 '16 at 15:43
2

Your solution is hard-coded to have inherent knowledge of the data structure (the nested for loops). A better solution would know nothing about the input data and still give you the expected result.

Either way, this is a pretty weird problem, but I was particularly bored so I figured I'd give it a shot. I mostly find this a completely pointless exercise because I cannot picture a scenario where the expected output could ever be better than the input.

This isn't a Rambda solution because there's no reason for it to be. You should understand the solution as a simple recursive procedure. If you can understand it, converting it to a sugary Rambda solution is trivial.

// determine if input is object
const isObject = x=> Object(x) === x

// flatten object
const oflatten = (data) => {
  let loop = (namespace, acc, data) => {
    if (Array.isArray(data))
      data.forEach((v,k)=>
        loop(namespace.concat([k]), acc, v))
    else if (isObject(data))
      Object.keys(data).forEach(k=>
        loop(namespace.concat([k]), acc, data[k]))
    else
      Object.assign(acc, {[namespace.join('.')]: data})
    return acc
  }
  return loop([], {}, data)
}

// example data
var fieldvalues = {
  name: "hello there",
  mobile: "1234", 
  meta: {status: "new"}, 
  comments: [ 
    {user: "john", comment: "hi"}, 
    {user: "ram", comment: "hello"}
  ]
}

// show me the money ...
console.log(oflatten(fieldvalues))

Total function

oflatten is reasonably robust and will work on any input. Even when the input is an array, a primitive value, or undefined. You can be certain you will always get an object as output.

// array input example
console.log(oflatten(['a', 'b', 'c']))
// {
//   "0": "a",
//   "1": "b",
//   "2": "c"
// }

// primitive value example
console.log(oflatten(5))
// {
//   "": 5
// }

// undefined example
console.log(oflatten())
// {
//   "": undefined
// }

How it works …

  1. It takes an input of any kind, then …

  2. It starts the loop with two state variables: namespace and acc . acc is your return value and is always initialized with an empty object {}. And namespace keeps track of the nesting keys and is always initialized with an empty array, []

    notice I don't use a String to namespace the key because a root namespace of '' prepended to any key will always be .somekey. That is not the case when you use a root namespace of [].

    Using the same example, [].concat(['somekey']).join('.') will give you the proper key, 'somekey'.

    Similarly, ['meta'].concat(['status']).join('.') will give you 'meta.status'. See? Using an array for the key computation will make this a lot easier.

  3. The loop has a third parameter, data, the current value we are processing. The first loop iteration will always be the original input

  4. We do a simple case analysis on data's type. This is necessary because JavaScript doesn't have pattern matching. Just because were using a if/else doesn't mean it's not functional paradigm.

  5. If data is an Array, we want to iterate through the array, and recursively call loop on each of the child values. We pass along the value's key as namespace.concat([k]) which will become the new namespace for the nested call. Notice, that nothing gets assigned to acc at this point. We only want to assign to acc when we have reached a value and until then, we're just building up the namespace.

  6. If the data is an Object, we iterate through it just like we did with an Array. There's a separate case analysis for this because the looping syntax for objects is slightly different than arrays. Otherwise, it's doing the exact same thing.

  7. If the data is neither an Array or an Object, we've reached a value. At this point we can assign the data value to the acc using the built up namespace as the key. Because we're done building the namespace for this key, all we have to do compute the final key is namespace.join('.') and everything works out.

  8. The resulting object will always have as many pairs as values that were found in the original object.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Thanks for a detailed answer and +1 for all the explanation. Recursion is the big takeaway (though I specifically left it out in my example). My intention was to understand FP, I took one example, that I was struggling with to make convert it from imperative way. I will explain (in the next 2 days) why I am using this JSON format -- which is very close to flattening the JSON, but the key is to work with MongoDB. – rsmoorthy Aug 01 '16 at 15:47