-
Notifications
You must be signed in to change notification settings - Fork 51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
filter, based on selective #101
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really cool. I think we should add it.
One note: users can implement filter, with less performance, using flatMap in the mean time (I guess any time you have Monad + Alternative this is true).
I had a couple concerns that I noted.
val res = genP.fa.filter(_ => false).parse(str) | ||
assert(res.isLeft) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we write a law on branch directly (see the laws I have on how ~ composes). I don't think what we have now is exactly right but since filter is weaker is might not detect it.
val either = parser.parseMut(state) | ||
if ((state.error eq null) && state.capture) | ||
either.fold(a => l.ap(Parser.pure(a)), b => r.ap(Parser.pure(b))).parseMut(state) | ||
else null.asInstanceOf[C] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this isn't right. I think if we are not capturing we still need to run the result because it may advance the offset in the parser.
Ugh. I didn't think anybody had looked at it yet, and just force-pushed it based on I'd also like to throw in an example and perhaps a bench to show how it compares to the monadic approach. |
Codecov Report
@@ Coverage Diff @@
## main #101 +/- ##
==========================================
- Coverage 96.44% 96.17% -0.28%
==========================================
Files 5 6 +1
Lines 676 732 +56
Branches 57 68 +11
==========================================
+ Hits 652 704 +52
- Misses 24 28 +4
Continue to review full report at Codecov.
|
Dude I check my GitHub notifications religiously. Well really more than most people do religious things. |
fn: Parser[A => B], | ||
state: State | ||
): B = { | ||
val either = parser.parseMut(state) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, look at flatMap. I think you have to set capture to true here unconditionally since otherwise we don't know what to do next.
After we capture the first, we can set capture to false on the second part since we can just avoid applying the function.
@@ -911,6 +914,9 @@ object Parser extends ParserInstances { | |||
case _ => Impl.Map1(p, fn) | |||
} | |||
|
|||
def select[A, B](p: Parser[Either[A, B]])(fn: Parser[A => B]): Parser[B] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For bonus points we can add select10 that takes a Parser1 then a Parser and returns a Parser1. Similarly a select01 that does the Parser1 second.
If we had that, we can override filter on Parser1 to return a Parser1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually with this formulation I think we only need select and select1. In select1 the first argument is a Parser1.
assertEquals(res0, res1) | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another interesting pair of laws: if we pa.ap(pb) should be the same as select(pa.map(Left), pb, fail)
Similarly on the right hand side.
@@ -1331,6 +1337,9 @@ object Parser extends ParserInstances { | |||
case Map(p, _) => | |||
// we discard any allocations done by fn | |||
unmap(p) | |||
case Select(p, _) => | |||
// we discard any allocations done by fn | |||
unmap(p) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need to do Select(a, b) => Select(a, unmap(b))
Since in select, after we optionally parse the right in a void we could not do anything more, but we do need the result on the left even in a void.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second parameter has to be a function, so I had to immediately and trivially map the unmapped value.
state.capture = cap | ||
if (state.error eq null) | ||
either match { | ||
case Left(a) => fn.map(_(a)).parseMut(state) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can do fun.parseMut(state) and in the capture case actually apply the function. In the non capture case you can just return null.
I think this also solves the unmap requirement of putting in const.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The .asInstanceOf
in the unmap
makes me queasy, but it makes the existing tests pass.
property("select(pa.map(Left(_)))(pf) == pf.ap(pa)") { | ||
forAll(ParserGen.gen, ParserGen.gen) { (genP, genRes) => | ||
val pa = genP.fa | ||
val pf = null: Parser[genP.A => genRes.A] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm struggling to produce this parser. The trick used in the generator didn't quite translate.
I made a PR as a suggestion to this one: It gets everything green, but changes somewhat the internal representation of Select (to work more cleanly with unmap). |
some suggestions for selective
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about merging this and publishing 0.2.0?
I think we need to remove the "Draft" label.
I'm good with that. I'd still like to figure out how to write that Left law, and I'm also interested in a benchmark that shows the advantage (or not) over flatMap. But none of that needs to hold back progress. |
Bump |
Actually I think adding an override for ifM using select: We should be able but I don't see it immediately. I assume we use select twice. |
Devil's advocacy: a risk in merging this before a benchmark is that the gains may be only theoretical and not justify the new primitive. I definitely want That said, I think it is better, and even if it's not, it's still 0.x. |
I bet ifM will be a lot faster using select (or a similar internal node). |
cats-selective and its Haskell forebears call it If this proves successful, I'd be interested in renewing the push to get Selective into Cats proper. With the right encoding, I think it could even be accepted into 2.x. |
This is really cool! 🎉 |
Riffing on the ideas of Staged Selective Parser Combinators.
Use case: in http4s, we need to parse various date headers into a 7-tuple of (weekday, year, month, day, hour, minute, second), and then verify that the date is valid (e.g., no 31st of February). Currently we have to flatMap it, and when parsing, we don't want to flatMap that shit.
Implemented on
branch
. We should have the full power of Selective. The primitive operation could also beselect
, which is the abstract function on cats-selective.Should be able to make a version that fails with a particular message, swapping that in for "empty".
There may be optimizations to sprinkle throughout the code. Fusing to
Impl.Map
seems possible, for instance.I'm super sleepy and just following types. This might all be nonsense.