1

The following script create an object filtering some input data. It is coded in a declarative way using several nested 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.

An example of code would be appreciate (specially for 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:

    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);
GibboK
  • 71,848
  • 143
  • 435
  • 658
  • can I ask you what you are trying to achieve? Do you want to do some filtering or something? – Stelios Voskos Dec 03 '16 at 11:07
  • @SteliosVoskos code filters some input data, rewrite some properties and return an object as result. I am interesting in learning functional programming. – GibboK Dec 03 '16 at 11:12
  • 1
    Ok, I am composing an answer with some good points for you. – Stelios Voskos Dec 03 '16 at 11:14
  • 1
    Of interest https://www.smashingmagazine.com/2014/07/dont-be-scared-of-functional-programming/ – GibboK Dec 03 '16 at 11:26
  • 1
    Of interest 2 http://stackoverflow.com/questions/35538351/ramda-js-lens-for-deeply-nested-objects-with-nested-arrays-of-objects – GibboK Dec 03 '16 at 11:34
  • 1
    Lenses https://medium.com/@drboolean/lenses-with-immutable-js-9bda85674780#.1jmyss7xc – GibboK Dec 03 '16 at 11:40
  • Also I would rename the title of the question to: `Converting imperative to functional paradigm style`. Declarative programming is synonymous to functional programming – Stelios Voskos Dec 03 '16 at 11:45

2 Answers2

3

Traversing complex structures by using just Ramda is hard but elegant. 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. But you are looking to transform the data in something very different to the original tree, which I assume is an AST. In Ramda, pipe and compose are essentials, it makes possible to structure the code by composing small functions. For working with trees I use converge for branching, objOf and zipObj to create new objects. Also map and reduce to work with lists.

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.

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

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.

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.

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)
yosbel
  • 339
  • 1
  • 4
  • 1
    Perfect answer! Many thanks for your time and expertise provided. :) – GibboK Dec 05 '16 at 20:27
  • 1
    This is a very nice breakdown. I spent some time on this and kept getting lost in the data structures. I did have it almost finished last night but sleep interfered. I may post a very different style answer tonight. – Scott Sauyet Dec 07 '16 at 12:51
  • 1
    Mine, BTW, started the same, with `pipe(path(['stylesheet', 'rules']), ` and a similar filter that used `where` rather than `propEq`. (`filter(where({type: equals('keyframes')})`) simply because I love the way that reads. – Scott Sauyet Dec 07 '16 at 12:55
  • 1
    I'm sure this example can be improved. Example, in `processKeyframe` I combine `map` and `reduce` which could be translated to tranducers. – yosbel Dec 07 '16 at 17:48
  • @YosbelMarin sorry to disturb you again, I would need also to convert offset / 100 example 50 => 0.5 I am now trying to fix it using [R.T, R.always(a => a / 100)] in processKeyframe but does not work, could you please me what is the best way to do it? Thanks – GibboK Dec 11 '16 at 09:16
  • 1
    @GibboK In that case you should replace `[R.T, parseFloat]` with `[R.T, R.pipe(parseFloat, R.divide(R.__, 100))]`. What is does is, first, executing `parseFloat` to convert from string, then divide the parsed number(deferred by the special placeholder `R.__`) by 100. – yosbel Dec 12 '16 at 14:14
  • 1
    @YosbelMarin thanks for explanation I was not aware of `R.__`. I take this opportunity to ask you another tip, in order to filter the result by offset asc, I haved modified the code in transformAST adding `R.map(R.pipe(R.sortBy(R.prop('offset'))))` as shown here https://jsfiddle.net/fwx3kmgo/2/ the script works, just would like to know if there is a better way to do it. Once again thanks for your time an expertise. – GibboK Dec 12 '16 at 14:42
  • 1
    @GibboK To accomplish that you should sort when creating the final object just after `flatten` where the array is appropiately fomatted. Check the jsfiddle out, https://jsfiddle.net/m9ddr4kv/1/ .Don't forget to rewrite `[R.equals('to'), R.always(100)]` to `[R.equals('to'), R.always(1)]`, since offsets are divided by 100 – yosbel Dec 12 '16 at 16:11
  • @YosbelMarin many thanks for your expertise in ramda :) . I really appreciate it. – GibboK Dec 13 '16 at 09:28
1

My version ends up looking quite different from the one by 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". (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:

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

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

And this bit replaces the if (v === 'from') { ... } else if ... code with a single expression:

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

that returns 0, 100, or the result of parseFloat(v), as appropriate.

Combining this with assoc('offset') and the result from the previous step we get the main objects in the result such as:

{
  "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).

You can see this in action on the 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!

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • thanks for your time and your professional answer, I appreciate that you have provided me another way to solve the problems. I found interesting the use of groupBy. One again thanks. – GibboK Dec 08 '16 at 05:37