Repository: jneen/parsimmon
Pull request #237 (2018-04-05 14:00:14)

Support Fantasy-Land Contravariant interface (and Profunctor by simple extension)
Refers to #236.

I'm not sure more than this is really necessary.  I am going to double check that the laws for contramap are upheld.


Patch:
diff --git a/API.md b/API.md
index 15ac0dc..e879a9f 100644
--- a/API.md
+++ b/API.md
@@ -58,6 +58,8 @@
   * [parser.chain(newParserFunc)](#parserchainnewparserfunc)
   * [parser.then(anotherParser)](#parserthenanotherparser)
   * [parser.map(fn)](#parsermapfn)
+  * [parser.contramap(fn)](#parsercontramapfn)
+  * [parser.promap(fn)](#parserpromapfn)
   * [parser.result(value)](#parserresultvalue)
   * [parser.fallback(value)](#parserfallbackvalue)
   * [parser.skip(otherParser)](#parserskipotherparser)
@@ -682,6 +684,58 @@ pNum.parse('123'); // => {status: true, value: 124}
 pNum.parse('3.1'); // => {status: true, value: 4.1}
 ```
 
+## parser.contramap(fn)
+
+Transforms the input of `parser` with the given function. Example:
+
+```javascript
+var pNum =
+  Parsimmon.string('A')
+    .contramap(function(x) {
+        return x.toUpperCase();
+    });
+
+pNum.parse('a');   // => {status: true, value: 'A'}
+pNum.parse('A'); // => {status: true, value: 'A'}
+```
+An important caveat of contramap is that it transforms the remaining input.  This means that you cannot expect values after a contramap in general, like the following.
+```javascript
+Parsimmon.seq(
+  Parsimmon.string('a'),
+  Parsimmon.string('c').contramap(function(x) {
+    return x.slice(1);
+  }),
+  Parsimmon.string('d')
+).tie().parse('abcd') //this will fail
+
+Parsimmon.seq(
+  Parsimmon.string("a"),
+  Parsimmon.seq(Parsimmon.string("c"), Parsimmon.string("d"))
+    .tie()
+    .contramap(function(x) {
+      return x.slice(1);
+    })
+).tie().parse('abcd') // => {status: true, value: 'acd'}
+```
+
+## parser.promap(fn)
+
+Transforms the input and output of `parser` with the given functions. Example:
+
+```javascript
+var pNum =
+  Parsimmon.string('A')
+    .promap(function(x) {
+        return x.toUpperCase();
+    }, function(x) {
+        return x.charCodeAt(0);
+    });
+
+pNum.parse('a');   // => {status: true, value: 65}
+pNum.parse('A'); // => {status: true, value: 65}
+```
+The same caveat for contramap above applies to promap.
+
 ## parser.result(value)
 
 Returns a new parser with the same behavior, but which yields `value`. Equivalent to `parser.map(function(x) { return x; }.bind(value))`.
diff --git a/src/parsimmon.js b/src/parsimmon.js
index d199d6b..97a370a 100644
--- a/src/parsimmon.js
+++ b/src/parsimmon.js
@@ -721,6 +721,24 @@ _.map = function(fn) {
   });
 };
 
+_.contramap = function(fn) {
+  assertFunction(fn);
+  var self = this;
+  return Parsimmon(function(input, i) {
+    var result = self.parse(fn(input.slice(i)));
+    if (!result.status) {
+      return result;
+    }
+    return makeSuccess(i + input.length, result.value);
+  });
+};
+
+_.promap = function(f, g) {
+  assertFunction(f);
+  assertFunction(g);
+  return this.contramap(f).map(g);
+};
+
 _.skip = function(next) {
   return seq(this, next).map(function(results) {
     return results[0];


Test patch:
diff --git a/test/core/contramap.test.js b/test/core/contramap.test.js
new file mode 100644
index 0000000..9bdae47
--- /dev/null
+++ b/test/core/contramap.test.js
@@ -0,0 +1,64 @@
+"use strict";
+
+suite("contramap", function() {
+  function toLower(x) {
+    return x.toLowerCase();
+  }
+
+  function chrs(b) {
+    return b.toString("ascii");
+  }
+
+  test("with a function, transforms the input and parses that", function() {
+    var parser = Parsimmon.string("x").contramap(function(x) {
+      return x.toLowerCase();
+    });
+    assert.deepEqual(parser.parse("X"), { status: true, value: "x" });
+  });
+
+  test("asserts that a function was given", function() {
+    assert.throws(function() {
+      Parsimmon.string("x").contramap("not a function");
+    });
+  });
+
+  test("upholds contravariant law of composition", function() {
+    var parser1 = Parsimmon.string("a")
+      .contramap(toLower)
+      .contramap(chrs);
+    var parser2 = Parsimmon.string("a").contramap(function(x) {
+      return toLower(chrs(x));
+    });
+    var input = Buffer.from([0x61]);
+
+    assert.deepEqual(parser1.parse(input), parser2.parse(input));
+  });
+
+  test("embedded contramaps make sense", function() {
+    var parser = Parsimmon.seq(
+      Parsimmon.string("a"),
+      Parsimmon.seq(Parsimmon.string("c"), Parsimmon.string("d"))
+        .tie()
+        .contramap(function(x) {
+          return x.slice(1);
+        })
+    ).tie();
+
+    assert.deepEqual(parser.parse("abcd"), { status: true, value: "acd" });
+  });
+
+  test("backtracking with contramaps works", function() {
+    var parser = Parsimmon.seq(
+      Parsimmon.string("a"),
+      Parsimmon.seq(Parsimmon.string("c"), Parsimmon.string("d"))
+        .tie()
+        .contramap(function(x) {
+          return x.slice(1);
+        })
+    )
+      .tie()
+      .or(Parsimmon.all);
+
+    assert.deepEqual(parser.parse("abcde"), { status: true, value: "abcde" });
+  });
+});
diff --git a/test/core/promap.test.js b/test/core/promap.test.js
new file mode 100644
index 0000000..095a313
--- /dev/null
+++ b/test/core/promap.test.js
@@ -0,0 +1,56 @@
+"use strict";
+
+suite("promap", function() {
+  function toLower(x) {
+    return x.toLowerCase();
+  }
+
+  function ord(chr) {
+    return chr.charCodeAt(0);
+  }
+
+  function chrs(b) {
+    return b.toString("ascii");
+  }
+
+  function length(x) {
+    return x.length;
+  }
+
+  function sub1(x) {
+    return x - 1;
+  }
+
+  test("with a function, transforms the input and parses that, and transforms the output", function() {
+    var parser = Parsimmon.string("a").promap(toLower, ord);
+    assert.deepEqual(parser.parse("A"), { status: true, value: 0x61 });
+  });
+
+  test("asserts that a function was given", function() {
+    assert.throws(function() {
+      Parsimmon.string("x").promap("not a function", toLower);
+    });
+
+    assert.throws(function() {
+      Parsimmon.string("x").promap(toLower, "not a function");
+    });
+  });
+
+  test("upholds profunctor law of composition", function() {
+    var parser1 = Parsimmon.string("aa")
+      .promap(toLower, length)
+      .promap(chrs, sub1);
+
+    var parser2 = Parsimmon.string("aa").promap(
+      function(x) {
+        return toLower(chrs(x));
+      },
+      function(x) {
+        return sub1(length(x));
+      }
+    );
+    var input = Buffer.from([0x41, 0x41]);
+
+    assert.deepEqual(parser1.parse(input), parser2.parse(input));
+  });
+});