Skip to content

Commit

Permalink
Implement FunctorFilter and add some more laws (#108)
Browse files Browse the repository at this point in the history
* Add mapOrFail, and some more laws

* Use FunctorFilter and names for Parsers
  • Loading branch information
johnynek committed Dec 10, 2020
1 parent bd8cef4 commit 437af75
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 14 deletions.
80 changes: 75 additions & 5 deletions core/shared/src/main/scala/cats/parse/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

package cats.parse

import cats.{Eval, Monad, Defer, Alternative, FlatMap, Now, MonoidK, Order}
import cats.{Eval, FunctorFilter, Monad, Defer, Alternative, FlatMap, Now, MonoidK, Order}
import cats.data.{AndThen, Chain, NonEmptyList}

import cats.implicits._
Expand Down Expand Up @@ -189,6 +189,33 @@ sealed abstract class Parser[+A] {
def map[B](fn: A => B): Parser[B] =
Parser.map(this)(fn)

/** Transform parsed values using the given function, or fail on None
*
* When the function return None, this parser fails
* This is implemented with select, which makes it more efficient
* than using flatMap
*/
def mapFilter[B](fn: A => Option[B]): Parser[B] = {
val leftUnit = Left(())

val first = map { a =>
fn(a) match {
case Some(b) => Right(b)
case None => leftUnit
}
}
Parser.select(first)(Parser.Fail)
}

/** Transform parsed values using the given function, or fail when not defined
*
* When the function is not defined, this parser fails
* This is implemented with select, which makes it more efficient
* than using flatMap
*/
def collect[B](fn: PartialFunction[A, B]): Parser[B] =
mapFilter(fn.lift)

/** If the predicate is not true, fail
* you may want .filter(fn).backtrack so if the filter fn
* fails you can fall through in an oneOf or orElse
Expand Down Expand Up @@ -357,11 +384,30 @@ sealed abstract class Parser1[+A] extends Parser[A] {
def <*[B](that: Parser[B]): Parser1[A] =
(this ~ that.void).map(_._1)

/** This method overrides `Parser#collect` to refine the return type.
*/
override def collect[B](fn: PartialFunction[A, B]): Parser1[B] =
mapFilter(fn.lift)

/** This method overrides `Parser#map` to refine the return type.
*/
override def map[B](fn: A => B): Parser1[B] =
Parser.map1(this)(fn)

/** This method overrides `Parser#mapFilter` to refine the return type.
*/
override def mapFilter[B](fn: A => Option[B]): Parser1[B] = {
val leftUnit = Left(())

val first = map { a =>
fn(a) match {
case Some(b) => Right(b)
case None => leftUnit
}
}
Parser.select1(first)(Parser.Fail)
}

/** This method overrides `Parser#flatMap` to refine the return type.
*/
override def flatMap[B](fn: A => Parser[B]): Parser1[B] =
Expand Down Expand Up @@ -1279,16 +1325,28 @@ object Parser extends ParserInstances {
case (notSingleChar, _) => notSingleChar.map(Impl.ConstFn(b))
}

implicit val catsInstancesParser1: FlatMap[Parser1] with Defer[Parser1] with MonoidK[Parser1] =
new FlatMap[Parser1] with Defer[Parser1] with MonoidK[Parser1] {
implicit val catsInstancesParser1
: FlatMap[Parser1] with Defer[Parser1] with MonoidK[Parser1] with FunctorFilter[Parser1] =
new FlatMap[Parser1] with Defer[Parser1] with MonoidK[Parser1] with FunctorFilter[Parser1] {
def empty[A] = Fail

def defer[A](pa: => Parser1[A]): Parser1[A] =
defer1(pa)

def functor = this

def map[A, B](fa: Parser1[A])(fn: A => B): Parser1[B] =
map1(fa)(fn)

def mapFilter[A, B](fa: Parser1[A])(f: A => Option[B]): Parser1[B] =
fa.mapFilter(f)

override def filter[A](fa: Parser1[A])(fn: A => Boolean): Parser1[A] =
fa.filter(fn)

override def filterNot[A](fa: Parser1[A])(fn: A => Boolean): Parser1[A] =
fa.filter { a => !fn(a) }

def flatMap[A, B](fa: Parser1[A])(fn: A => Parser1[B]): Parser1[B] =
flatMap10(fa)(fn)

Expand Down Expand Up @@ -2175,16 +2233,28 @@ object Parser extends ParserInstances {
}

abstract class ParserInstances {
implicit val catInstancesParser: Monad[Parser] with Alternative[Parser] with Defer[Parser] =
new Monad[Parser] with Alternative[Parser] with Defer[Parser] {
implicit val catInstancesParser
: Monad[Parser] with Alternative[Parser] with Defer[Parser] with FunctorFilter[Parser] =
new Monad[Parser] with Alternative[Parser] with Defer[Parser] with FunctorFilter[Parser] {
def pure[A](a: A): Parser[A] = Parser.pure(a)

def defer[A](a: => Parser[A]) = Parser.defer(a)

def empty[A]: Parser[A] = Parser.Fail

def functor = this

override def map[A, B](fa: Parser[A])(fn: A => B): Parser[B] = Parser.map(fa)(fn)

def mapFilter[A, B](fa: Parser[A])(f: A => Option[B]): Parser[B] =
fa.mapFilter(f)

override def filter[A](fa: Parser[A])(fn: A => Boolean): Parser[A] =
fa.filter(fn)

override def filterNot[A](fa: Parser[A])(fn: A => Boolean): Parser[A] =
fa.filter { a => !fn(a) }

override def product[A, B](fa: Parser[A], fb: Parser[B]): Parser[(A, B)] =
Parser.product(fa, fb)

Expand Down
125 changes: 116 additions & 9 deletions core/shared/src/test/scala/cats/parse/ParserTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -496,10 +496,29 @@ object ParserGen {
)
}

def genParser[A](genA: Gen[A]): Gen[Parser[A]] =
for {
genT <- gen
fn <- Gen.function1(genA)(genT.cogen)
} yield genT.fa.map(fn)

def genParser1[A](genA: Gen[A]): Gen[Parser1[A]] =
for {
genT <- gen1
fn <- Gen.function1(genA)(genT.cogen)
} yield genT.fa.map(fn)

implicit def arbParser[A: Arbitrary]: Arbitrary[Parser[A]] =
Arbitrary(genParser(Arbitrary.arbitrary[A]))

implicit def arbParser1[A: Arbitrary]: Arbitrary[Parser1[A]] =
Arbitrary(genParser1(Arbitrary.arbitrary[A]))
}

class ParserTest extends munit.ScalaCheckSuite {

import ParserGen.{arbParser, arbParser1}

val tests: Int = if (BitSetUtil.isScalaJs) 50 else 2000

override def scalaCheckTestParameters =
Expand Down Expand Up @@ -1517,6 +1536,20 @@ class ParserTest extends munit.ScalaCheckSuite {
}
}

property("a.backtrack.peek.orElse(b.peek) == (a.backtrack.orElse(b)).peek") {
forAll(ParserGen.gen, ParserGen.gen, Arbitrary.arbitrary[String]) { (a, b, str) =>
val pa = a.fa.backtrack
val pb = b.fa

val left = pa.peek.orElse(pb.peek)
val right = pa.orElse(pb).peek

val leftRes = left.parse(str).toOption
val rightRes = right.parse(str).toOption
assertEquals(leftRes, rightRes)
}
}

property("a.peek == a.peek *> a.peek") {
forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (a, str) =>
val pa = a.fa.peek
Expand Down Expand Up @@ -1580,6 +1613,28 @@ class ParserTest extends munit.ScalaCheckSuite {
}
}

property("!fail == unit") {
forAll { (str: String) =>
val left = !Parser.fail
val right = Parser.unit

val leftRes = left.parse(str)
val rightRes = right.parse(str)
assertEquals(leftRes, rightRes)
}
}

property("!pure(_) == fail") {
forAll { (str: String, i: Int) =>
val left = !Parser.pure(i)
val right = Parser.fail

val leftRes = left.parse(str).toOption
val rightRes = right.parse(str).toOption
assertEquals(leftRes, rightRes)
}
}

property("anyChar.repAs[String] parses the whole string") {
forAll { (str: String) =>
assertEquals(Parser.anyChar.repAs[String].parse(str), Right(("", str)))
Expand Down Expand Up @@ -1615,24 +1670,36 @@ class ParserTest extends munit.ScalaCheckSuite {
}
}

/*
property("select(pa.map(Left(_)))(pf) == (pa, pf).mapN((a, fn) => fn(a))") {
forAll(ParserGen.gen, ParserGen.gen, Arbitrary.arbitrary[String]) { (genP, genRes, str) =>
val pa = genP.fa
val pf = null: Parser[genP.A => genRes.A]
assertEquals(Parser.select(pa.map(Left(_)))(pf).parse(str), pf.ap(pa).parse(str))
forAll { (pa: Parser[Int], pf: Parser[Int => String], str: String) =>
assertEquals(
Parser.select(pa.map(Left(_)))(pf).parse(str),
(pa, pf).mapN((a, f) => f(a)).parse(str)
)
}
}

property("select1(pa.map(Left(_)))(pf) == (pa, pf).mapN((a, fn) => fn(a))") {
forAll { (pa: Parser1[Int], pf: Parser[Int => String], str: String) =>
assertEquals(
Parser.select(pa.map(Left(_)))(pf).parse(str),
(pa, pf).mapN((a, f) => f(a)).parse(str)
)
}
}
*/

property("select(pa.map(Right(_)))(pf) == pa") {
forAll(ParserGen.gen, ParserGen.gen, Arbitrary.arbitrary[String]) { (genP, genRes, str) =>
val pa = genRes.fa
val pf: Parser[genP.A => genRes.A] = Parser.fail
forAll { (pa: Parser[String], pf: Parser[Int => String], str: String) =>
assertEquals(Parser.select(pa.map(Right(_)))(pf).parse(str), pa.parse(str))
}
}

property("select1(pa.map(Right(_)))(pf) == pa") {
forAll { (pa: Parser1[String], pf: Parser[Int => String], str: String) =>
assertEquals(Parser.select1(pa.map(Right(_)))(pf).parse(str), pa.parse(str))
}
}

property("p.filter(_ => true) == p") {
forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (genP, str) =>
val res0 = genP.fa.filter(_ => true).parse(str)
Expand Down Expand Up @@ -1671,4 +1738,44 @@ class ParserTest extends munit.ScalaCheckSuite {
)
}
}

property("mapFilter is the same as filter + map") {
forAll { (pa: Parser[Int], fn: Int => Option[String], str: String) =>
val left = pa.mapFilter(fn)
val right = pa.map(fn).filter(_.isDefined).map(_.get)

assertEquals(left.parse(str), right.parse(str))
}
}

property("mapFilter is the same as filter + map Parser1") {
forAll { (pa: Parser1[Int], fn: Int => Option[String], str: String) =>
val left = pa.mapFilter(fn)
val right = pa.map(fn).filter(_.isDefined).map(_.get)

assertEquals(left.parse(str), right.parse(str))
}
}

property("collect is the same as filter + map") {
forAll { (pa: Parser[Int], fn: Int => Option[String], str: String) =>
val left = pa.collect {
case i if fn(i).isDefined => fn(i).get
}
val right = pa.map(fn).filter(_.isDefined).map(_.get)

assertEquals(left.parse(str), right.parse(str))
}
}

property("collect is the same as filter + map Parser1") {
forAll { (pa: Parser1[Int], fn: Int => Option[String], str: String) =>
val left = pa.collect {
case i if fn(i).isDefined => fn(i).get
}
val right = pa.map(fn).filter(_.isDefined).map(_.get)

assertEquals(left.parse(str), right.parse(str))
}
}
}

0 comments on commit 437af75

Please sign in to comment.