Functional Javascript - 以 FP 方式转换为点分格式(使用 Ramda)

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

我正在 Javascript 学习函数式编程并使用 Ramda。我有这个对象

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

要这样转换:

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

我试过这个 Ramda 源码,效果很好。

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));

但是,我不确定这是否接近函数式编程方法。它似乎只是围绕着一些功能代码。

任何想法或指示,我可以用这种更实用的方式编写这段代码。

代码片段可用 here

更新 1

更新了 code 以解决旧数据被标记的问题。

谢谢

您的解决方案被硬编码为具有数据结构的固有知识(嵌套的 for 循环)。更好的解决方案应该对输入数据一无所知,但仍会为您提供预期的结果。

无论如何,这是一个非常奇怪的问题,但我特别无聊所以我想我会试一试。我主要发现这是一个完全没有意义的练习,因为我无法想象预期输出 曾经 比输入更好的场景。

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))

总函数

oflatten 相当稳健,适用于任何输入。即使输入是数组、原始值或 undefined。你可以肯定你总是会得到一个对象作为输出。

// 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
// }

它是如何工作的……

  1. 它接受任何类型的输入,然后……

  2. 它以两个状态变量开始循环:namespaceaccacc 是您的 return 值,并且始终使用空对象 {} 进行初始化。 namespace 跟踪嵌套键并始终使用空数组初始化,[]

    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. 循环有第三个参数,data,我们正在处理的当前值。第一次循环迭代将始终是原始输入

  4. 我们对data的类型做一个简单的案例分析。这是必要的,因为 JavaScript 没有模式匹配。仅仅因为我们使用了 if/else 并不意味着它不是功能范例。

  5. 如果 data 是一个数组,我们想要遍历数组,并递归调用每个子值的 loop。我们将值的键作为 namespace.concat([k]) 传递,它将成为嵌套调用的新命名空间。请注意,此时没有任何内容分配给 acc。我们只想在达到 value 时分配给 acc,在此之前,我们只是在构建命名空间。

  6. 如果 data 是一个对象,我们会像处理数组一样遍历它。对此有一个单独的案例分析,因为对象的循环语法与数组略有不同。否则,它会做同样的事情。

  7. 如果 data 既不是数组也不是对象,我们就达到了 。此时我们可以使用构建的 namespace 作为键将 data 值分配给 acc。因为我们已经完成了为这个键构建命名空间,所以我们所要做的就是计算最终的键 namespace.join('.') 一切都会成功。

  8. 生成的对象将始终具有与在原始对象中找到的值一样多的对。

函数式方法

  • 使用递归处理任意形状的数据
  • 使用多个微型函数作为构建块
  • 对数据使用模式匹配以根据具体情况选择计算方式

无论您是将可变对象作为累加器传递(为了性能)还是复制属性(为了纯度)并不重要,只要最终结果(在您的 public API) 是不可变的。实际上,您已经使用了第三种不错的方法:关联列表(键值对),这将简化 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))

除了连接键并将它们作为参数传递之外,您还可以将 R.prepend(key) 映射到每个 dot 调用的结果。