1

I have a data structure I call a 'spec' which looks like this:

const spec = {
  command: {
    name: 'name',
    description: 'description',
    alias: 'alias',
    arguments: '_children/Arguments'
  },
  arguments: {
    name: 'name',
    alias: 'alias',
    optional: 'optional',
    description: 'description'
  }
};

So the elements inside of command and arguments are properties mapped to paths. The best illustration of this is spec.command.arguments. What I need to do is translate this into another object with the same shape, but the paths are converted into Ramda lenses (using R.lensPath).

So conceptually, this is translated into something like this:

const spec = {
  command: {
    name: lens('name'),
    description: lens('description'),
    alias: lens('alias'),
    arguments: lens('_children/Arguments')
  },
  arguments: {
    name: lens('name'),
    alias: lens('alias'),
    optional: lens('optional'),
    description: lens('description')
  }
};

The above is not meant to be taken literally, it is a pseudo structure. For example lens('_children/Arguments') just represents a lens built using Ramda lensPath.

So here is my code:

const spec = {
  command: {
    name: 'name',
    description: 'description',
    alias: 'alias',
    arguments: '_children/Arguments'
  },
  arguments: {
    name: 'name',
    alias: 'alias',
    optional: 'optional',
    description: 'description'
  }
};

function lensify (spec) {
  const result = R.pipe(
    R.toPairs,
    R.reduce((acc, pair) => {
      const field = pair[0];
      const path = pair[1];
      const lens = R.compose(
        R.lensPath,
        R.split('/')
      )(path);

      acc[field] = lens; // Is there something wrong with this, if so what?
      return acc;
    }, { dummy: '***' }) // list of pairs passed as last param here
  )(spec);

  // The following log should show entries for 'name', 'description', 'alias' ...
  console.log(`+++ lensify RESULT: ${JSON.stringify(result)}`);
  return result;
}

function makeLenses (spec) {
  const result = {
    command: lensify(spec.command),
    arguments: lensify(spec.arguments)
  };

  return result;
}

makeLenses(spec);

The key point of failure I think is inside the reducer function, which returns the updated accumulator (acc[field] = lens;). For some reason which I can't understand, this assignment is being lost, and the accumulator is not being correctly populated on each iteration. As you can see from the code sample, the initial value passed into reduce is an object with a single dummy property. The result of the reduce is incorrectly just this single dummy value and not all the fields with their respective Ramda lenses.

However, what's really gonna bake your noodle is that the exact same code running in Ramda repl exhibits different behaviour, see this code in the repl at: Ramda code

I'm running node version 10.13.0

The result that the Repl code produces is this:

{
  'arguments': {
    'alias': function (r) {
      return function (e) {
        return z(function (t) {
          return n(t, e)
        }, r(t(e)))
      }
    },
    'description': function (r) {
      return function (e) {
        return z(function (t) {
          return n(t, e)
        }, r(t(e)))
      }
    },
    'dummy': '***',
    'name': function (r) {
      return function (e) {
        return z(function (t) {
          return n(t, e)
        }, r(t(e)))
      }
    },
    'optional': function (r) {
      return function (e) {
        return z(function (t) {
          return n(t, e)
        }, r(t(e)))
      }
    }
  },
  'command': {
    'alias': function (r) {
      return function (e) {
        return z(function (t) {
          return n(t, e)
        }, r(t(e)))
      }
    },
    'arguments': function (r) {
      return function (e) {
        return z(function (t) {
          return n(t, e)
        }, r(t(e)))
      }
    },
    'description': function (r) {
      return function (e) {
        return z(function (t) {
          return n(t, e)
        }, r(t(e)))
      }
    },
    'dummy': '***',
    'name': function (r) {
      return function (e) {
        return z(function (t) {
          return n(t, e)
        }, r(t(e)))
      }
    }
  }
}

As you can see, the result looks a bit complicated because the values of each property is the lens created by lensProp.

This is in contrast to the following (note that order of command and arguments is reversed, but this shouldn't be significant):

{
  'command': {
    'dummy': '***'
  },
  'arguments': {
    'dummy': '***'
  }
}

which is being returned in my unit test.

I've wasted about 2 days on this and have now admitted defeat, so hopefully, somebody can shed some light on this. Cheers.

11
  • 1
    I'm really not sure what your question is here. The results in the REPL look correct to me. (Note that JSON.stringify won't give you useful results on function properties.) You return an object with arguments and command properties, each of which is an object containing name, alias arguments and optional properties, each holding a lens. Is that not what you want? Commented May 31, 2019 at 13:59
  • 1
    Note that, as long as you don't actually want that dummy property, you can write lensify much more simply as const lensify = map(pipe(split('/'), lensPath)). Commented May 31, 2019 at 14:04
  • 1
    Your final REPL result is not wrapped in JSON.stringify. It uses the REPL's more sophisticated display feature. JSON.stringify(makeLenses(spec)) //=> "{\"command\":{\"dummy\":\"***\"},\"arguments\":{\"dummy\":\"***\"}}" Commented May 31, 2019 at 14:06
  • 1
    Also, you can skip lensify altogether with const makeLenses = map(map(pipe(split('/'), lensPath))), assuming that you want to apply it to all elements of your spec. Commented May 31, 2019 at 14:11
  • 1
    Actually, I realised I made a big boo boo in some of my client code, which is responsible for the undefined I was seeing trying to access the lenses. It work now and produces the same result as the Repl. Thanks for you help Scott. I'll use your much more streamlined version in my final code, although I already knew I was going to streamline it. One thing I wasnt aware of though was the enhanced display abilities of the repl, which is what confused me. The console.log/JSON.stringify statement I was using was not showing the lens properties that the Repl does, which sent me down a rabbit hole!. Commented May 31, 2019 at 14:37

2 Answers 2

1

This shows the simplest usage of your output I can imagine, mapping view on the lenses against a common object. It seems to work properly both in the REPL, here in a snippet, and in Node 10.13.0:

const {map, pipe, split, lensPath, view} = ramda  

const makeLenses = map ( map ( pipe ( split ('/'), lensPath )))

const applyLensSpec = (lensSpec) => (obj) => 
  map ( map ( f => view (f, obj) ), lensSpec)

const spec = {command: {name: "name", description: "description", alias: "alias", arguments: "_children/Arguments"}, arguments: {name: "name", alias: "alias", optional: "optional", description: "description"}};

const myTransform = applyLensSpec(
  makeLenses(spec),
)

const testObj =   {
  name: 'foo', 
  alias: 'bar', 
  description: 'baz', 
  optional: false, 
  _children: {
    Arguments: ['qux', 'corge']
  }
}

console .log (
  myTransform (testObj)
)
<script src="https://bundle.run/[email protected]"></script>

Sign up to request clarification or add additional context in comments.

3 Comments

Thats great Scott, cheers. My problem was 2 two fold, incorrect client code that was not shown in this post and confusion about JSON.stringify and how this can be different to Ramda repl
The REPL tries to be more helpful than JSON.stringify. This is the first time I've heard of confusion caused by that, but I can certainly understand how it might happen!
Well I'm sure in the fullness of time and more acquired knowledge, I'm sure it\ll turn out that I misunderstood something or did something wrong. It all part of my javascript journey. Thanks
0

An addendum to this post and to conform what Scott said, the reason for this post is the deficiency on JSON.stringify and this is in fact the moral of this story; do not always trust the output of JSON.stringify. Here is a test case that confirms this:

  context('JSON.stringify', () => {
    it.only('spec/lensSpec', () => {
      const spec = {
        command: {
          name: 'name',
          description: 'description',
          alias: 'alias',
          arguments: '_children/Arguments'
        },
        arguments: {
          name: 'name',
          alias: 'alias',
          optional: 'optional',
          description: 'description'
        }
      };

      const makeLensSpec = R.map(R.map(R.pipe(
        R.split('/'),
        R.lensPath
      )));

      const lensSpec = makeLensSpec(spec);
      console.log(`INPUT spec: ${JSON.stringify(spec)}`);
      // The following stringify does not truly reflect the real value of lensSpec.
      // So do not trust the output of JSON.stringify when the value of a property
      // is a function as in this case where they are the result of Ramda.lensProp.
      //
      console.log(`RESULT lensSpec: ${JSON.stringify(lensSpec)}`);
      const rename = {
        'name': 'rename',
        'alias': 'rn',
        'source': 'filesystem-source',
        '_': 'Command',
        'describe': 'Rename albums according to arguments specified.',
        '_children': {
          'Arguments': {
            'with': {
              'name': 'with',
              '_': 'Argument',
              'alias': 'w',
              'optional': 'true',
              'describe': 'replace with'
            },
            'put': {
              'name': 'put',
              '_': 'Argument',
              'alias': 'pu',
              'optional': 'true',
              'describe': 'update existing'
            }
          }
        }
      };

      // NB, if the output of JSON.stringify was indeed correct, then this following
      // line would not work; ie accessing lensSpec.command would result in undefined,
      // but this is not the case; the lensSpec can be used to correctly retrieve the
      // command name.
      //
      const name = R.view(lensSpec.command.name, rename);
      console.log(`COMMAND name: ${name}`);
    });
  });

The log statements of note are:

console.log(INPUT spec: ${JSON.stringify(spec)});

which displays this:

INPUT spec: {"command":{"name":"name","description":"description","alias":"alias","arguments":"_children/Arguments"},"arguments":{"name":"name","alias":"alias","optional":"optional","description":"description"}}

console.log(RESULT lensSpec: ${JSON.stringify(lensSpec)});

This is the one at fault (lensSpec contains properties whose values are functions which stringify can't display, so misses them out entirely, giving an incorrect representation:

RESULT lensSpec: {"command":{},"arguments":{}}

console.log(COMMAND name: ${name});

This works as expected:

COMMAND name: rename

NB: I just found this: Why doesn't JSON.stringify display object properties that are functions?

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.