简体   繁体   中英

Functional Javascript - Convert to dotted format in FP way (uses Ramda)

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

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM