简体   繁体   English

使用Ramdajs将命令式转换为功能样式范例

[英]Converting Imperative to Functional style paradigm using Ramdajs

The following script create an object filtering some input data. 以下脚本创建一个对象,该对象过滤一些输入数据。 It is coded in a declarative way using several nested forEach . 它使用几个嵌套的forEach以声明的方式进行编码。

I would like to know which API to use in rewritting this code using ramdajs or lodash , specially I would be interested in understand if use of pipe is appropriate in this case otherwise another way. 我想知道在使用ramdajslodash重新编写此代码时使用哪个API,特别是我想了解在这种情况下使用管道是否合适,否则我会感兴趣。

An example of code would be appreciate (specially for ramdajs). 将提供一个示例代码(特别是对于ramdajs)。 Thanks. 谢谢。

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

EDIT: 编辑:

So far I was able to achieve this result in ramdajs: 到目前为止,我已经能够在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);

Traversing complex structures by using just Ramda is hard but elegant. 仅使用Ramda遍历复杂的结构既困难又优雅。 To modify a structure using lenses, applySpec and evolve is recommendable, these are very useful to return a new version of objects with modified values. 要使用的镜头,修改结构applySpecevolve是值得推荐的,这些都是返回对象的新版本与修改后的值是非常有用的。 But you are looking to transform the data in something very different to the original tree, which I assume is an AST. 但是您正在寻求以与原始树非常不同的方式转换数据,我认为这是AST。 In Ramda, pipe and compose are essentials, it makes possible to structure the code by composing small functions. 在Ramda中, pipecompose是必不可少的,这使得通过编写小的函数来构造代码成为可能。 For working with trees I use converge for branching, objOf and zipObj to create new objects. 为了使用树,我使用converge进行分支,使用objOfzipObj创建新对象。 Also map and reduce to work with lists. 还要mapreduce以使用列表。

I'm going to use the following composition strategy in this example: 在此示例中,我将使用以下组合策略:

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

To start with, let's create a function that receives an array of values and an array of declarations , it returns an array which in the first position has an array of converted values, in the second position an object where the keys are the value of declaration property and the values are its corresponding declaration value . 首先,让我们创建一个函数,该函数接收一个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))
]

Now let's create a function to process animations, it receives an array of offsets and an object with transformations, returns an array of new objects with the signature {offset: offset, ...trasformations} . 现在,让我们创建一个处理动画的函数,它接收offsets数组和一个带有转换的对象,返回带有签名{offset: offset, ...trasformations}的新对象数组。

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

Next, map each keyframe by composing the two previous functions 接下来,通过组合前面的两个函数来映射每个关键帧

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

Finally, we define function that gets the needed properties from data , summarizes, each keyframe and finally gives the desired format in the last stage. 最后,我们定义一个函数,该函数从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))
    ]))

Now you can process the AST directly passing the data object. 现在,您可以处理直接传递data对象的AST。

var result = transformAST(data)

All together. 全部一起。

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)

My version ends up looking quite different from the one by Yosbel Marin. 我的版本最终看起来与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)
);

I did this as a code port without really trying to understand your data at all. 我这样做是作为代码端口,而没有真正尝试完全理解您的数据。 (I was having a hard time doing so, this was at least in part a necessity, but it's also an interesting way to proceed.) (我很难这么做,至少在某种程度上这是必须的,但这也是一种有趣的方式。)

The first two steps should be clear, and they are very similar to the previous answer. 前两个步骤应该很清楚,它们与先前的答案非常相似。 We grab the data from data.stylesheet.rules , then we filter it to only include those rules whose "type" property is "keyframes". 我们从data.stylesheet.rules获取数据,然后对其进行过滤,以仅包括“ type”属性为“ keyframes”的那些规则。 (I chose to use where in my filter, as I find the following more readable than propEq : filter(where({'type': equals('keyframes')})) , but they work the same. This is followed by groupBy(prop('name')) , which leaves us with a structure like: (我选择在过滤器中使用where ,因为我发现以下内容比propEq更具可读性: filter(where({'type': equals('keyframes')})) ,但它们的工作原理相同。之后是groupBy(prop('name')) ,这给我们留下了一个像这样的结构:

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

The next bit is the heart of the transformation. 接下来是转型的核心。 I converted each of the forEach calls in the original into map calls (obviously one can't always do this.) 我将原始forEach中的每个forEach调用都转换为map调用(显然不能总是这样做。)

This: 这个:

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

turns a declarations section into something like 将声明部分变成类似

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

by lifting the objOf function from working on scalar values to work on functions that return such values, and then passing in two functions which will accept a declaration. 通过将objOf函数从处理标量值提升为处理返回此类值的函数,然后传入两个将接受声明的函数。 This new function then accepts a declaration and returns an object. 然后,此新函数接受一个声明并返回一个对象。 Mapping it over a list of declarations gets a list of objects. 将其映射到声明列表将获得对象列表。 Putting it into a pipe call with mergeAll turns such a list into a single object. 使用mergeAll将其放入pipe调用中会将这样的列表变成单个对象。

And this bit replaces the if (v === 'from') { ... } else if ... code with a single expression: 并且此位用单个表达式替换if (v === 'from') { ... } else if ...代码:

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

that returns 0 , 100 , or the result of parseFloat(v) , as appropriate. 返回0100 ,或的结果parseFloat(v)根据。

Combining this with assoc('offset') and the result from the previous step we get the main objects in the result such as: 将其与assoc('offset')和上一步的结果结合起来,我们得到的主要对象如下:

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

The only thing left to do is to clean up the nested lists left by all those maps: 剩下要做的唯一一件事就是清理所有这些地图留下的嵌套列表:

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

which we do by adding map(flatten) . 我们通过添加map(flatten)

You can see this in action on the Ramda REPL . 您可以在Ramda REPL上看到这一点。

I have no idea if this could reasonably be made entirely points-free. 我不知道是否可以合理地将其完全免费化。 I am guessing that it would be difficult at best, and that it would end up much less readable. 我猜测这充其量是困难的,最终其可读性将大大降低。 This code might do well with factoring some of the functions that are being mapped into their own calls, but I'll leave that as an exercise for the reader! 这段代码可以很好地分解一些映射到自己的调用中的函数,但是我将其留给读者练习!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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