简体   繁体   中英

Refactoring if statements using Ramda

I'm new to using Ramda so when I performed a map function (in my gatsby app) on an array of blog posts, within that map I used simple if statements. But I want to do this the right way, using Ramda all the way through, but I'm finding its many options overwhelming. I've definitely found and tried many examples from StackOverflow and elsewhere and have been able to grab the categories but not the whole blog post based on those categories. So while I realize there are examples out there I am struggling to apply them correctly in my instance. Any opinions about how I would refactor this code?

    const featured_post = [];
    const hrt_category = [];
    const work_category = [];
    const prep_category = [];
    const ed_category = [];

    {map(
        ({ node }) => {

            if (node.categories.some(e => e.slug === 'featured')) {
                featured_post.push(node);
            }
            if (node.categories.some(e => e.slug === 'hormones')) {
                hrt_category.push(node);
            }
            if (node.categories.some(e => e.slug === 'estrogen')) {
                work_category.push(node);
            }
            if (node.categories.some(e => e.slug === 'prep')) {
                prep_category.push(node);
            }
            if (node.categories.some(e => e.slug === 'ed')) {
                ed_category.push(node);
            }

        },posts
    )}

Any help is much appreciated.

To my mind, this is a misuse of Ramda (disclaimer: I'm a Ramda founder.)

Ramda is all about working with immutable data. Although the internals of Ramda functions may mutate local variables, they never alter anything else.

Using map to create side-effects such as pushing to global variables is definitely a mismatch for Ramda.

Besides that, I am not a fan of this repetitive code. My take would be radically different. I wouldn't use all those global collections of posts.

Here's one possibility:

 const groupPosts = (names, posts) => Object.fromEntries (names.map (name => [ name, posts.filter ( ({node: {categories}}) => categories.some (({slug}) => slug == name) ).map (({node}) => node) ])) const posts = [{node: {id: 1, categories: [{slug: 'prep'}, {slug: 'ed'}]}}, {node: {id: 2, categories: [{slug: 'estrogen'}]}}, {node: {id: 3, categories: [{slug: 'ed'}]}}, {node: {id: 4, categories: [{slug: 'hormones'}, {slug: 'prep'}]}}, {node: {id: 5, categories: [{slug: 'featured'}]}}, {node: {id: 6, categories: [{slug: 'prep'}]}}, {node: {id: 7, categories: [{slug: 'estroogen'}]}},] console.log (JSON.stringify ( groupPosts (['featured', 'hormones', 'estrogen', 'prep', 'ed'], posts), null, 2))
 .as-console-wrapper {max-height: 100%;important: top: 0}

But if you want those individual subcollections, we could extract from this the function that matches a single set of them:

const matchSlugs = (name) => (posts) =>
  posts .filter (
    ({node: {categories}}) => categories .some (({slug}) => slug == name)
  ) .map (({node}) => node)

and we would use it like const featured_posts = matchSlugs ('featured') (posts) and our original function would be an easy gloss:

const sortPosts = (names) => (posts) =>
  Object .fromEntries (names .map (name => [name, matchSlugs (name) (posts)]))

Note that this has not used Ramda anywhere. We could start changing it to use Ramda's nicer map and filter functions, to use toPairs in place of Object.fromEntries , possibly to use Ramda's any in place of .some . It might make this a bit cleaner, and I would recommend that you try if you're in the process of learning Ramda. But that is the icing. The cake is in simplifying the data structures and the code that use them.

Update

The OP posted a Ramda-based refactoring of matchSlugs and asked for feedback.

Here is a series of refactorings of that version to a fully point-free version:

  1. The OP's solution (with my own layout since comments don't allow us to display layout):

     const matchSlugs = (name) => (posts) => map ( ({node}) => node, filter( ({node: {categories}}) => any ((({slug}) => slug == name)) (categories), posts ) );
  2. After pulling out the posts parameter:

     const matchSlugs2 = (name) => pipe ( filter(({node: {categories}}) => any ((({slug}) => slug == name)) (categories)), map (prop ('node')) )

    This version separates the filtering of existing nodes from the final mapping of the results, and by putting those two steps into a pipe call, allows us to remove the second argument. Note that pipe takes some functions and returns a function. This retains the behavior above.

  3. After cleaning up the destructured parameter in filter :

     const matchSlugs3 = (name) => pipe ( filter ( pipe ( path (['node', 'categories']), any (({slug}) => slug == name) ) ), map (prop ('node')) )

    Here we use path (['node', 'categories']) to replace the ({node: {categories}} parameter. That involves another pipe call, which I hope to clean up later.

  4. After replacing the anonymous function with propEq

    const matchSlugs4 = (name) => pipe ( filter (pipe ( path (['node', 'categories']), any (propEq ('slug', name)) )), map (prop ('node')) )

    This is just a minor fix, but propEq reads more cleanly to me than the lambda function. It also will allow us to do the next refactoring.

  5. After refactoring to remove the name parameter:

     const matchSlugs5 = pipe ( propEq ('slug'), any, flip (o) (path (['node', 'categories'])), filter, o (map (prop ('node'))) )

    This is the largest step here. We turn this into a point-free function, using two instances of o , Ramda's curried binary version of compose . This is very useful in making functions point-free, but can feel fairly obscure. (The name o is meant to remind us of the mathematical composition sign, .) The first time we need to flip o , since Ramda does not have a pipe -style version of o .

For some Ramda users, this would be the ultimate goal, achieving a point-free version of the function. There are times when that is quite readable, but I personally feel that this final step has reduced readability. I might go back to an earlier pointed version, or, even better, to a version inspired by this one, but with the points reinstated:

const matchSlugs6 = (name) => (posts) =>
  map (
    prop('node'), 
    filter (
      compose (any (propEq ('slug', name)), path (['node', 'categories'])),
      posts
    )
  )

To me this version is the most readable of the bunch. But tastes differ, and you may find another one of these or an entirely different version most readable.

You can reduce the nodes to an object of nodes by slugs, and then pick the requested slug in the stated order:

 const { mergeWith, concat, reduce, pick } = R const mergeConcat = mergeWith(concat) const groupPosts = (names, posts) => pick( // pick the names you want in the correct order names, reduce((p, { node }) => mergeConcat(p, // merge with the general categories reduce((c, { slug }) => // create an object of node by categories ({...c, [slug]: [node] }), {}, node.categories) ), {}, posts) ) const posts = [{node: {id: 1, categories: [{slug: 'prep'}, {slug: 'ed'}]}}, {node: {id: 2, categories: [{slug: 'estrogen'}]}}, {node: {id: 3, categories: [{slug: 'ed'}]}}, {node: {id: 4, categories: [{slug: 'hormones'}, {slug: 'prep'}]}}, {node: {id: 5, categories: [{slug: 'featured'}]}}, {node: {id: 6, categories: [{slug: 'prep'}]}}, {node: {id: 7, categories: [{slug: 'estroogen'}]}},] console.log (JSON.stringify ( groupPosts (['featured', 'hormones', 'estrogen', 'prep', 'ed'], posts), null, 2))
 .as-console-wrapper {max-height: 100%;important: top: 0}
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>

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.

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