Skip to content
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

Add Align instance #643

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion core/shared/src/main/scala/cats/parse/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
package cats.parse

import cats.{
Align,
Eval,
Functor,
FunctorFilter,
Expand All @@ -34,7 +35,7 @@ import cats.{
Order,
Show
}
import cats.data.{AndThen, Chain, NonEmptyList}
import cats.data.{AndThen, Chain, Ior, NonEmptyList}

import cats.implicits._
import scala.collection.immutable.SortedSet
Expand Down Expand Up @@ -1582,6 +1583,36 @@ object Parser {
case _ => Impl.SoftProd(first, second)
}

/** This implements the main method from the Align typeclass. This parses the first then maybe the
* second, or just the second. Put another way, it parses at least one of the arguments.
*/
def align[A, B](pa: Parser[A], pb: Parser[B]): Parser[Ior[A, B]] = {
val hasA = (pa ~ pb.?)
.map {
case (a, Some(b)) => Ior.Both(a, b)
case (a, None) => Ior.Left(a)
}

val onlyB = pb.map(Ior.Right(_))

hasA | onlyB
}

/** This implements the main method from the Align typeclass This parses the first then maybe the
* second, or just the second. Put another way, it parses at least one of the arguments.
*/
def align0[A, B](pa: Parser0[A], pb: Parser0[B]): Parser0[Ior[A, B]] = {
val hasA = (pa ~ pb.?)
.map {
case (a, Some(b)) => Ior.Both(a, b)
case (a, None) => Ior.Left(a)
}

val onlyB = pb.map(Ior.Right(_))

hasA | onlyB
}

/** transform a Parser0 result
*/
def map0[A, B](p: Parser0[A])(fn: A => B): Parser0[B] =
Expand Down Expand Up @@ -2159,6 +2190,13 @@ object Parser {

}

implicit val catsAlignParser: Align[Parser] =
new Align[Parser] {
def functor = catsInstancesParser
def align[A, B](pa: Parser[A], pb: Parser[B]): Parser[Ior[A, B]] =
Parser.align(pa, pb)
}

/*
* This is protected rather than private to avoid a warning on 2.12
*/
Expand Down Expand Up @@ -3692,4 +3730,11 @@ object Parser0 {
}

}

implicit val catsAlignParser0: Align[Parser0] =
new Align[Parser0] {
def functor = catInstancesParser0
def align[A, B](pa: Parser0[A], pb: Parser0[B]): Parser0[Ior[A, B]] =
Parser.align0(pa, pb)
}
}
45 changes: 44 additions & 1 deletion core/shared/src/test/scala/cats/parse/ParserTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
package cats.parse

import cats.arrow.FunctionK
import cats.data.{NonEmptyList, NonEmptyVector}
import cats.data.{Ior, NonEmptyList, NonEmptyVector}
import cats.implicits._
import cats.{Eq, Id, FlatMap, Functor, Defer, MonoidK, Monad, Eval}
import org.scalacheck.Prop.forAll
Expand Down Expand Up @@ -3066,4 +3066,47 @@ class ParserTest extends munit.ScalaCheckSuite {
assertEquals(left.parse(str), right.parse(str))
}
}

private def alignAssoc[A, B, C](x: Ior[A, Ior[B, C]]): Ior[Ior[A, B], C] = {
import Ior._
x match {
case Left(a) => Left(Left(a))
case Right(bc) =>
bc match {
case Left(b) => Left(Right(b))
case Right(c) => Right(c)
case Both(b, c) => Both(Right(b), c)
}
case Both(a, bc) =>
bc match {
case Left(b) => Left(Both(a, b))
case Right(c) => Both(Left(a), c)
case Both(b, c) => Both(Both(a, b), c)
}
}
}

property("align is associative") {
forAll(ParserGen.gen, ParserGen.gen, ParserGen.gen, arbitrary[String]) { (a, b, c, str) =>
val alignInst = cats.Align[Parser]
import alignInst.align

val left = align(a.fa, align(b.fa, c.fa)).map(alignAssoc)
val right = align(align(a.fa, b.fa), c.fa)

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

property("align0 is associative") {
forAll(ParserGen.gen0, ParserGen.gen0, ParserGen.gen0, arbitrary[String]) { (a, b, c, str) =>
val alignInst = cats.Align[Parser0]
import alignInst.align

val left = align(a.fa, align(b.fa, c.fa)).map(alignAssoc)
val right = align(align(a.fa, b.fa), c.fa)

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