Skip to content

Commit

Permalink
Merge pull request #237 from theqabalist/master
Browse files Browse the repository at this point in the history
Support Fantasy-Land Contravariant interface (and Profunctor by simple extension)
  • Loading branch information
Brian Mock committed Jun 17, 2018
2 parents da33f6e + d3bf01b commit f3bfabe
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 0 deletions.
54 changes: 54 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,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)
Expand Down Expand Up @@ -725,6 +727,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))`.
Expand Down
18 changes: 18 additions & 0 deletions src/parsimmon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
64 changes: 64 additions & 0 deletions test/core/contramap.test.js
Original file line number Diff line number Diff line change
@@ -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" });
});
});
56 changes: 56 additions & 0 deletions test/core/promap.test.js
Original file line number Diff line number Diff line change
@@ -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));
});
});

0 comments on commit f3bfabe

Please sign in to comment.