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 …
It takes an input of any kind, then …
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.
The loop has a third parameter, data
, the current value we are processing. The first loop iteration will always be the original input
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.
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.
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.
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.
The resulting object will always have as many pairs as values that were found in the original object.
A functional approach would
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.