【问题标题】:Converting Imperative to Functional style paradigm using Ramdajs使用 Ramdajs 将命令式转换为函数式范例
【发布时间】:2017-04-18 05:23:42
【问题描述】:

以下脚本创建一个过滤某些输入数据的对象。 它使用多个嵌套的forEach 以声明方式编码。

我想知道使用ramdajslodash 重写此代码时使用哪个API,特别是我有兴趣了解在这种情况下使用pipe 是否合适,否则另一种方式。

一个代码示例将不胜感激(特别是对于 ramdajs)。谢谢。

  var data = {
    "type": "stylesheet",
    "stylesheet": {
      "rules": [{
        "type": "keyframes",
        "name": "bounce",
        "keyframes": [{
          "type": "keyframe",
          "values": [
            "from",
            "20%",
            "53%",
            "80%",
            "to"
          ],
          "declarations": [{
            "type": "declaration",
            "property": "animation-timing-function",
            "value": "cubic-bezier(0.215, 0.610, 0.355, 1.000)",
            "position": {
              "start": {
                "line": 3,
                "column": 5
              },
              "end": {
                "line": 3,
                "column": 72
              }
            }
          }, {
            "type": "declaration",
            "property": "transform",
            "value": "translate3d(0,0,0)",
            "position": {
              "start": {
                "line": 4,
                "column": 5
              },
              "end": {
                "line": 4,
                "column": 34
              }
            }
          }],
          "position": {
            "start": {
              "line": 2,
              "column": 3
            },
            "end": {
              "line": 5,
              "column": 4
            }
          }
        }, {
          "type": "keyframe",
          "values": [
            "40%",
            "43%"
          ],
          "declarations": [{
            "type": "declaration",
            "property": "animation-timing-function",
            "value": "cubic-bezier(0.755, 0.050, 0.855, 0.060)",
            "position": {
              "start": {
                "line": 8,
                "column": 5
              },
              "end": {
                "line": 8,
                "column": 72
              }
            }
          }, {
            "type": "declaration",
            "property": "transform",
            "value": "translate3d(0, -30px, 0)",
            "position": {
              "start": {
                "line": 9,
                "column": 5
              },
              "end": {
                "line": 9,
                "column": 40
              }
            }
          }],
          "position": {
            "start": {
              "line": 7,
              "column": 3
            },
            "end": {
              "line": 10,
              "column": 4
            }
          }
        }, {
          "type": "keyframe",
          "values": [
            "70%"
          ],
          "declarations": [{
            "type": "declaration",
            "property": "animation-timing-function",
            "value": "cubic-bezier(0.755, 0.050, 0.855, 0.060)",
            "position": {
              "start": {
                "line": 13,
                "column": 5
              },
              "end": {
                "line": 13,
                "column": 72
              }
            }
          }, {
            "type": "declaration",
            "property": "transform",
            "value": "translate3d(0, -15px, 0)",
            "position": {
              "start": {
                "line": 14,
                "column": 5
              },
              "end": {
                "line": 14,
                "column": 40
              }
            }
          }],
          "position": {
            "start": {
              "line": 12,
              "column": 3
            },
            "end": {
              "line": 15,
              "column": 4
            }
          }
        }, {
          "type": "keyframe",
          "values": [
            "90%"
          ],
          "declarations": [{
            "type": "declaration",
            "property": "transform",
            "value": "translate3d(0,-4px,0)",
            "position": {
              "start": {
                "line": 18,
                "column": 5
              },
              "end": {
                "line": 18,
                "column": 37
              }
            }
          }],
          "position": {
            "start": {
              "line": 17,
              "column": 3
            },
            "end": {
              "line": 19,
              "column": 4
            }
          }
        }],
        "position": {
          "start": {
            "line": 1,
            "column": 1
          },
          "end": {
            "line": 20,
            "column": 2
          }
        }
      }, {
        "type": "rule",
        "selectors": [
          ".bounce"
        ],
        "declarations": [{
          "type": "declaration",
          "property": "animation-name",
          "value": "bounce",
          "position": {
            "start": {
              "line": 23,
              "column": 3
            },
            "end": {
              "line": 23,
              "column": 25
            }
          }
        }, {
          "type": "declaration",
          "property": "transform-origin",
          "value": "center bottom",
          "position": {
            "start": {
              "line": 24,
              "column": 3
            },
            "end": {
              "line": 24,
              "column": 34
            }
          }
        }],
        "position": {
          "start": {
            "line": 22,
            "column": 1
          },
          "end": {
            "line": 25,
            "column": 2
          }
        }
      }, {
        "type": "keyframes",
        "name": "spark",
        "keyframes": [{
          "type": "keyframe",
          "values": [
            "0%",
            "50%"
          ],
          "declarations": [{
            "type": "declaration",
            "property": "transform",
            "value": "translate3d(0,0,0)",
            "position": {
              "start": {
                "line": 29,
                "column": 5
              },
              "end": {
                "line": 29,
                "column": 34
              }
            }
          }],
          "position": {
            "start": {
              "line": 28,
              "column": 3
            },
            "end": {
              "line": 30,
              "column": 4
            }
          }
        }, {
          "type": "keyframe",
          "values": [
            "100%"
          ],
          "declarations": [{
            "type": "declaration",
            "property": "transform",
            "value": "translate3d(0,-4px,0)",
            "position": {
              "start": {
                "line": 32,
                "column": 5
              },
              "end": {
                "line": 32,
                "column": 37
              }
            }
          }],
          "position": {
            "start": {
              "line": 31,
              "column": 3
            },
            "end": {
              "line": 33,
              "column": 4
            }
          }
        }],
        "position": {
          "start": {
            "line": 27,
            "column": 1
          },
          "end": {
            "line": 34,
            "column": 2
          }
        }
      }, {
        "type": "rule",
        "selectors": [
          ".spark"
        ],
        "declarations": [{
          "type": "declaration",
          "property": "animation-name",
          "value": "spark",
          "position": {
            "start": {
              "line": 37,
              "column": 3
            },
            "end": {
              "line": 37,
              "column": 24
            }
          }
        }, {
          "type": "declaration",
          "property": "transform-origin",
          "value": "center center",
          "position": {
            "start": {
              "line": 38,
              "column": 3
            },
            "end": {
              "line": 38,
              "column": 34
            }
          }
        }],
        "position": {
          "start": {
            "line": 36,
            "column": 1
          },
          "end": {
            "line": 39,
            "column": 2
          }
        }
      }],
      "parsingErrors": []
    }
  };
  var result = {};
  var kfs = data.stylesheet.rules.filter(function(rule) {
    return rule.type === 'keyframes'
  });

  kfs.forEach(function(kf) {
    result[kf.name] = [];
    kf.keyframes.forEach(function(kfi) {
      kfi.values.forEach(function(v) {
        var r = {};
        var vNew;
        vNew = v;
        if (v === 'from') {
          vNew = 0;
        } else if (v === 'to') {
          vNew = 100;
        } else {
          vNew = parseFloat(v);
        }
        r.offset = vNew;
        kfi.declarations.forEach(function(d) {
          r[d.property] = d.value;

        });
        result[kf.name].push(r);
      });
    });
  });
  console.log(result);

编辑:

到目前为止,我能够在 ramdajs 中实现这个结果:

    var rulesLense = R.lensPath(['stylesheet', 'rules']);
    var ruleView = R.view(rulesLense, obj);
    var keyframes = R.filter(R.propEq('type', 'keyframes'));
    var groupByKeyframe = R.groupBy(keyframe => {
        return R.prop('name', keyframe);
    });

    var process = R.pipe(
        keyframes,
        groupByKeyframe  
    );
    var result = process(ruleView);

【问题讨论】:

标签: javascript functional-programming lodash ramda.js


【解决方案1】:

仅使用 Ramda 遍历复杂的结构是困难但优雅的。要使用镜头修改结构,建议使用applySpecevolve,它们对于返回具有修改值的新版本对象非常有用。但是您希望将数据转换为与原始树非常不同的东西,我认为原始树是 AST。在 Ramda 中,pipecompose 是必不可少的,它可以通过组合小函数来构造代码。对于树,我使用converge 进行分支,objOfzipObj 用于创建新对象。还可以使用 mapreduce 处理列表。

我将在本例中使用以下组合策略:

          transformAST
               ^
               |
               |
      getContentOfKeyframes
         ^              ^
         |              |
         |              |
  processKeyframe   processAnimation

首先,让我们创建一个接收values数组和declarations数组的函数,它返回一个数组,在第一个位置有一个转换值数组,在第二个位置有一个对象键是声明property 的值,值是其对应的声明value

var processKeyframe = (vals, declarations) => [
    // map each value
    R.map(R.cond([
        [R.equals('from'), R.always(0)],
        [R.equals('to'), R.always(100)],
        [R.T, parseFloat]
    ]), vals),
    // collect all property value pairs and merge in one object
    R.reduce(R.merge, {},
        R.map(R.converge(R.objOf, [
            R.prop('property'),
            R.prop('value')
        ]), declarations))
]

现在让我们创建一个处理动画的函数,它接收一个 offsets 数组和一个带有变换的对象,返回一个带有签名 {offset: offset, ...trasformations} 的新对象数组。

var processAnimation = (offsets, transf) => 
    R.map(R.pipe(
        R.objOf('offset'), 
        R.merge(transf)), offsets)

接下来,通过组合前面的两个函数来映射每个关键帧

var getContentOfKeyframes = R.map(R.pipe(
    // process keyframes
    R.converge(processKeyframe, [
        R.prop('values'),
        R.prop('declarations')
    ]),
    // process animations
    R.converge(processAnimation, [
        R.nth(0),
        R.nth(1)
    ])))

最后,我们定义从data获取所需属性的函数,汇总每个关键帧,最后在最后阶段给出所需的格式。

var transformAST = R.pipe(
    // get `stylesheet.rules` property
    R.path(['stylesheet', 'rules']),
    // get only object whose `type` property is `keyframes`
    R.filter(R.propEq('type', 'keyframes')), 
    // map each item in `keyframes` collection
    // to an object {name: keyframe.name, content: [contentOfkeyframes] }
    R.map((keyframe) => ({
        name    : keyframe.name,
        content : getContentOfKeyframes(keyframe.keyframes)
    })),
    // finally make a new object using animation `name` as keys
    // and using a flatten content as values
    R.converge(R.zipObj, [
        R.map(R.prop('name')),
        R.map(R.pipe(R.prop('content'), R.flatten))
    ]))

现在您可以直接通过 data 对象处理 AST。

var result = transformAST(data)

一起来。

var processKeyframe = (vals, declarations) => [
    R.map(R.cond([
        [R.equals('from'), R.always(0)],
        [R.equals('to'), R.always(100)],
        [R.T, parseFloat]
    ]), vals),
    R.reduce(R.merge, {},
        R.map(R.converge(R.objOf, [
            R.prop('property'),
            R.prop('value')
        ]), declarations))
]

var processAnimation = (offsets, transf) => 
    R.map(R.pipe(
        R.objOf('offset'), 
        R.merge(transf)), offsets)

var getContentOfKeyframes = R.map(R.pipe(
    R.converge(processKeyframe, [
        R.prop('values'),
        R.prop('declarations')
    ]),
    R.converge(processAnimation, [
        R.nth(0),
        R.nth(1)
    ])))

var transformAST = R.pipe(
    R.path(['stylesheet', 'rules']),
    R.filter(R.propEq('type', 'keyframes')), 
    R.map((keyframe) => ({
        name    : keyframe.name,
        content : getContentOfKeyframes(keyframe.keyframes)
    })),
    R.converge(R.zipObj, [
        R.map(R.prop('name')),
        R.map(R.pipe(R.prop('content'), R.flatten))
    ]))

var result = transformAST(data)

【讨论】:

  • 完美答案!非常感谢您提供的时间和专业知识。 :)
  • 这是一个非常好的细分。我花了一些时间在这上面,并且一直迷失在数据结构中。我昨晚几乎完成了,但睡眠受到干扰。今晚我可能会发布一个风格迥异的答案。
  • 我的,顺便说一句,以pipe(path(['stylesheet', 'rules']), 和使用where 而不是propEq 的类似过滤器开始。 (filter(where({type: equals('keyframes')})) 只是因为我喜欢这种阅读方式。
  • 我确信这个例子可以改进。例如,在processKeyframe 中,我结合了mapreduce,它们可以被翻译成转换器。
  • @GibboK 在这种情况下,您应该将[R.T, parseFloat] 替换为[R.T, R.pipe(parseFloat, R.divide(R.__, 100))]。所做的是,首先执行parseFloat从字符串转换,然后将解析后的数字(由特殊占位符R.__延迟)除以100。
【解决方案2】:

我的版本最终看起来与 Yosbel Marin 的版本完全不同。

const transform = pipe(
  path(['stylesheet', 'rules']),
  filter(where({'type': equals('keyframes')})),
  groupBy(prop('name')),
  map(map(kf => map(kfi => map(v => assoc('offset', cond([
      [equals('from'), always(0)],
      [equals('to'), always(100)],
      [T, parseFloat]
    ])(v), pipe(
        map(lift(objOf)(prop('property'), prop('value'))), 
        mergeAll
    )(kfi.declarations)), kfi.values), kf.keyframes)
  )),
  map(flatten)
);

我将其作为代码移植,根本没有真正尝试理解您的数据。 (我很难做到这一点,这至少部分是必要的,但它也是一种有趣的方式。)

前两步应该很清楚,和上一个答案很相似。我们从data.stylesheet.rules 中获取数据,然后过滤它以仅包含“type”属性为“keyframes”的那些规则。 (我选择在我的过滤器中使用where,因为我发现以下内容比propEq更具可读性:filter(where({'type': equals('keyframes')})),但它们的工作方式相同。紧随其后的是groupBy(prop('name')),这给我们留下了如下结构:

{
  bounce: [obj1, obj2, ...]
  spark: [objA, objB, ...]
}

下一点是转型的核心。我将原始中的每个forEach 调用都转换为map 调用(显然不能总是这样做。)

这个:

map(v => map(lift(objOf)(prop('property'), prop('value'))), kfi.declarations)

将声明部分变成类似

[
  {"animation-timing-function": "cubic-bezier(0.215, 0.610, 0.355, 1.000)",}
  {transform: "translate3d(0,0,0)"},
]

通过将objOf 函数从处理标量值提升到处理返回此类值的函数,然后传入两个将接受声明的函数。然后这个新函数接受一个声明并返回一个对象。将其映射到声明列表会得到对象列表。将其放入带有mergeAllpipe 调用中会将这样的列表变成单个对象。

这个位用一个表达式替换了if (v === 'from') { ... } else if ... 代码:

cond([
  [equals('from'), always(0)],
  [equals('to'), always(100)],
  [T, parseFloat]
])(v)

根据需要返回0100parseFloat(v) 的结果。

将其与assoc('offset') 和上一步的结果相结合,我们得到结果中的主要对象,例如:

{
  "animation-timing-function": "cubic-bezier(0.215, 0.610, 0.355, 1.000)",
  offset: 0,
  transform: "translate3d(0,0,0)"
}

剩下要做的就是清理所有这些地图留下的嵌套列表:

{
  bounce: [[[obj1, obj2, ...]]]
  spark: [[[objA, objB, ...]]]
}

我们通过添加map(flatten)来做到这一点。

您可以在 Ramda REPL 上看到这一点。

我不知道这是否可以合理地完全免费。我猜这充其量是困难的,而且它最终的可读性会大大降低。这段代码可能会很好地分解一些被映射到它们自己的调用中的函数,但我将把它留给读者作为练习!

【讨论】:

  • 感谢您的时间和专业的回答,感谢您为我提供了另一种解决问题的方法。我发现 groupBy 的使用很有趣。再次感谢。
猜你喜欢
  • 1970-01-01
  • 2011-09-30
  • 1970-01-01
  • 2016-01-08
  • 2012-10-20
  • 1970-01-01
  • 2013-10-11
  • 2012-07-02
  • 2011-01-30
相关资源
最近更新 更多