Skip to content

Commit

Permalink
Combinators that return cats.data.NonEmptyList (#5)
Browse files Browse the repository at this point in the history
* Added combinator module with non-empty versions of the regular combinators

* Added full qualification for combinator

* Added documentation for combinators

* Module documentation

* Fixed another qualification

* Fixed imports

* Fixed copyright headers and imports (though this looks like a scalastyle bug...

* Used irrefutable match, it's just cleaner

* Added tests
  • Loading branch information
j-mie6 authored Feb 7, 2023
1 parent 5324fcc commit 3faf945
Show file tree
Hide file tree
Showing 10 changed files with 343 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package parsley.cats

import parsley.Parsley
import parsley.combinator

import cats.{Functor, MonoidK}

Expand All @@ -15,6 +14,6 @@ private [parsley] trait MonoidKForParsley extends MonoidK[Parsley] {

// MonoidK Overrides
override def sum[A, B](mx: Parsley[A], my: Parsley[B])(implicit F: Functor[Parsley]): Parsley[Either[A,B]] = mx <+> my
override def combineAllK[A](ps: TraversableOnce[Parsley[A]]): Parsley[A] = combinator.choice(ps.toIterator.toSeq: _*)
override def combineAllK[A](ps: TraversableOnce[Parsley[A]]): Parsley[A] = parsley.combinator.choice(ps.toIterator.toSeq: _*)
override def combineAllOptionK[A](ps: TraversableOnce[Parsley[A]]): Option[Parsley[A]] = ps.toIterator.reduceRightOption(_<|>_)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package parsley.cats

import parsley.Parsley
import parsley.combinator

import cats.{Functor, MonoidK}

Expand All @@ -15,6 +14,6 @@ private [parsley] trait MonoidKForParsley extends MonoidK[Parsley] {

// MonoidK Overrides
override def sum[A, B](mx: Parsley[A], my: Parsley[B])(implicit F: Functor[Parsley]): Parsley[Either[A,B]] = mx <+> my
override def combineAllK[A](ps: IterableOnce[Parsley[A]]): Parsley[A] = combinator.choice(ps.iterator.toSeq: _*)
override def combineAllK[A](ps: IterableOnce[Parsley[A]]): Parsley[A] = parsley.combinator.choice(ps.iterator.toSeq: _*)
override def combineAllOptionK[A](ps: IterableOnce[Parsley[A]]): Option[Parsley[A]] = ps.iterator.reduceRightOption(_<|>_)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
*/
package parsley.cats

import cats.Applicative

import parsley.Parsley
import parsley.combinator
import parsley.lift._

import cats.Applicative

private [parsley] trait ApplicativeForParsley extends Applicative[Parsley] {
override def pure[A](x: A): Parsley[A] = Parsley.pure(x)
override def unit: Parsley[Unit] = Parsley.unit
Expand All @@ -20,8 +19,8 @@ private [parsley] trait ApplicativeForParsley extends Applicative[Parsley] {
override def product[A, B](mx: Parsley[A], my: Parsley[B]): Parsley[(A, B)] = mx <~> my
override def ap[A, B](mf: Parsley[A => B])(mx: Parsley[A]): Parsley[B] = mf <*> mx

override def replicateA[A](n: Int, mx: Parsley[A]): Parsley[List[A]] = combinator.exactly(n, mx)
override def replicateA_[A](n: Int, mx: Parsley[A]): Parsley[Unit] = combinator.skip(mx, (1 until n).map(_ => mx): _*)
override def replicateA[A](n: Int, mx: Parsley[A]): Parsley[List[A]] = parsley.combinator.exactly(n, mx)
override def replicateA_[A](n: Int, mx: Parsley[A]): Parsley[Unit] = parsley.combinator.skip(mx, (1 until n).map(_ => mx): _*)

// Maps and Tuples
override def map2[A, B, Z](mx: Parsley[A], my: Parsley[B])(f: (A, B) => Z): Parsley[Z] = lift2(f, mx, my)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
*/
package parsley.cats

import parsley.Parsley, Parsley.LazyParsley

import cats.Defer

import parsley.Parsley, Parsley.LazyParsley

private [parsley] class DeferForParsley extends Defer[Parsley] {
def defer[A](p: =>parsley.Parsley[A]): parsley.Parsley[A] = ~p
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
*/
package parsley.cats

import parsley.Parsley

import cats.Functor

import parsley.Parsley

private [parsley] trait FunctorForParsley extends Functor[Parsley] {
override def map[A, B](mx: Parsley[A])(f: A => B): Parsley[B] = mx.map(f)
override def as[A, B](mx: Parsley[A], y: B): Parsley[B] = mx #> y
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package parsley.cats

import parsley.Parsley
import parsley.combinator
import parsley.registers.{RegisterMaker, RegisterMethods}

import cats.{Alternative, Monad}
Expand All @@ -18,11 +17,11 @@ private [parsley] trait MonadForParsley extends Monad[Parsley] {
}

// Monad Overrides
override def ifM[B](mx: Parsley[Boolean])(ifTrue: => Parsley[B], ifFalse: => Parsley[B]): Parsley[B] = combinator.ifP(mx, ifTrue, ifFalse)
override def ifM[B](mx: Parsley[Boolean])(ifTrue: => Parsley[B], ifFalse: => Parsley[B]): Parsley[B] = parsley.combinator.ifP(mx, ifTrue, ifFalse)
override def whileM_[A](p: Parsley[Boolean])(body: =>Parsley[A]): Parsley[Unit] = {
combinator.when(p, combinator.whileP(body ~> p))
parsley.combinator.when(p, parsley.combinator.whileP(body ~> p))
}
override def untilM_[A](body: Parsley[A])(p: => Parsley[Boolean]): Parsley[Unit] = combinator.whileP(body *> p.map(!_))
override def untilM_[A](body: Parsley[A])(p: => Parsley[Boolean]): Parsley[Unit] = parsley.combinator.whileP(body *> p.map(!_))

override def whileM[G[_]: Alternative, A](p: Parsley[Boolean])(body: => Parsley[A]): Parsley[G[A]] = {
val G = implicitly[Alternative[G]]
Expand All @@ -39,25 +38,25 @@ private [parsley] trait MonadForParsley extends Monad[Parsley] {
}

override def untilDefinedM[A](mox: Parsley[Option[A]]): Parsley[A] = {
lazy val loop: Parsley[A] = combinator.decide(mox, loop)
lazy val loop: Parsley[A] = parsley.combinator.decide(mox, loop)
loop
}

override def iterateUntil[A](mx: Parsley[A])(p: A => Boolean): Parsley[A] = {
lazy val loop: Parsley[A] = mx.persist { mx =>
combinator.ifP(mx.map(p), mx, loop)
parsley.combinator.ifP(mx.map(p), mx, loop)
}
loop
}
override def iterateWhile[A](mx: Parsley[A])(p: A => Boolean): Parsley[A] = {
lazy val loop: Parsley[A] = mx.persist { mx =>
combinator.ifP(mx.map(p), loop, mx)
parsley.combinator.ifP(mx.map(p), loop, mx)
}
loop
}
override def ifElseM[A](branches: (Parsley[Boolean], Parsley[A])*)(els: Parsley[A]): Parsley[A] = {
branches.foldRight(els) {
case ((cond, t), e) => combinator.ifP(cond, t, e)
case ((cond, t), e) => parsley.combinator.ifP(cond, t, e)
}
}
}
159 changes: 159 additions & 0 deletions parsley-cats/shared/src/main/scala/parsley/cats/combinator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/* SPDX-FileCopyrightText: © 2023 Parsley Cats Contributors <https://github.com/j-mie6/parsley-cats/graphs/contributors>
* SPDX-License-Identifier: BSD-3-Clause
*/
package parsley.cats

import cats.data.NonEmptyList

import parsley.Parsley, Parsley.notFollowedBy
import parsley.combinator.{many, manyUntil}
import parsley.lift.lift2

/** This module contains pre-made combinators that are very useful for a variety of purposes, specialised to `cats`.
*
* In particular, it contains functionality found normally in `parsley.combinator`, but returning the `cats` `NonEmptyList`
* instead of a regular Scala `List`.
*
* @since 1.2.0
*/
object combinator {
private [cats] def nonEmptyList[A](p: Parsley[A], ps: =>Parsley[List[A]]) = lift2[A, List[A], NonEmptyList[A]](NonEmptyList(_, _), p, ps)

/** This combinator repeatedly parses a given parser '''one''' or more times, collecting the results into a list.
*
* Parses a given parser, `p`, repeatedly until it fails. If `p` failed having consumed input,
* this combinator fails. Otherwise when `p` fails '''without consuming input''', this combinator
* will return all of the results, `x,,1,,` through `x,,n,,` (with `n >= 1`), in a non-empty list: `NonEmptyList.of(x,,1,,, .., x,,n,,)`.
* If `p` was not successful at least one time, this combinator fails.
*
* @example {{{
* scala> import parsley.character.string
* scala> import parsley.combinator.some
* scala> val p = some(string("ab"))
* scala> p.parse("")
* val res0 = Failure(..)
* scala> p.parse("ab")
* val res1 = Success(NonEmptyList.of("ab"))
* scala> p.parse("abababab")
* val res2 = Success(NonEmptyList.of("ab", "ab", "ab", "ab"))
* scala> p.parse("aba")
* val res3 = Failure(..)
* }}}
*
* @param p the parser to execute multiple times.
* @return a parser that parses `p` until it fails, returning the non-empty list of all the successful results.
* @since 1.2.0
*/
def some[A](p: Parsley[A]): Parsley[NonEmptyList[A]] = nonEmptyList(p, many(p))

/** This combinator repeatedly parses a given parser '''one''' or more times, until the `end` parser succeeds, collecting the results into a list.
*
* First ensures that trying to parse `end` fails, then tries to parse `p`. If it succeed then it will repeatedly: try to parse `end`, if it fails
* '''without consuming input''', then parses `p`, which must succeed. When `end` does succeed, this combinator will return all of the results
* generated by `p`, `x,,1,,` through `x,,n,,` (with `n >= 1`), in a non-empty list: `NonEmptyList.of(x,,1,,, .., x,,n,,)`. The parser `p` must succeed
* at least once before `end` succeeds.
*
* @example This can be useful for scanning comments: {{{
* scala> import parsley.character.{string, item, endOfLine}
* scala> import parsley.combinator.many
* scala> val comment = string("//") *> someUntil(item, endOfLine)
* scala> p.parse("//hello world")
* val res0 = Failure(..)
* scala> p.parse("//hello world\n")
* val res1 = Success(NonEmptyList.of('h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'))
* scala> p.parse("//\n")
* val res2 = Failure(..)
* scala> p.parse("//a\n")
* val res3 = Success(NonEmptyList.of('a'))
* }}}
*
* @param p the parser to execute multiple times.
* @param end the parser that stops the parsing of `p`.
* @return a parser that parses `p` until `end` succeeds, returning the non-empty list of all the successful results.
* @since 1.2.0
*/
def someUntil[A](p: Parsley[A], end: Parsley[_]): Parsley[NonEmptyList[A]] = notFollowedBy(end) *> (nonEmptyList(p, manyUntil(p, end)))

/** This combinator parses '''one''' or more occurrences of `p`, separated by `sep`.
*
* First parses a `p`. Then parses `sep` followed by `p` until there are no more `sep`s.
* The results of the `p`'s, `x,,1,,` through `x,,n,,`, are returned as `NonEmptyList.of(x,,1,,, .., x,,n,,)`.
* If `p` or `sep` fails having consumed input, the whole parser fails. Requires at least
* one `p` to have been parsed.
*
* @example {{{
* scala> ...
* scala> val args = sepBy1(int, string(", "))
* scala> args.parse("7, 3, 2")
* val res0 = Success(NonEmptyList.of(7, 3, 2))
* scala> args.parse("")
* val res1 = Failure(..)
* scala> args.parse("1")
* val res2 = Success(NonEmptyList.of(1))
* scala> args.parse("1, 2, ")
* val res3 = Failure(..) // no trailing comma allowed
* }}}
*
* @param p the parser whose results are collected into a list.
* @param sep the delimiter that must be parsed between every `p`.
* @return a parser that parses `p` delimited by `sep`, returning the non-empty list of `p`'s results.
* @since 1.2.0
*/
def sepBy1[A](p: Parsley[A], sep: =>Parsley[_]): Parsley[NonEmptyList[A]] = nonEmptyList(p, many(sep *> p))

/** This combinator parses '''one''' or more occurrences of `p`, separated and optionally ended by `sep`.
*
* First parses a `p`. Then parses `sep` followed by `p` until there are no more: if a final `sep` exists, this is parsed.
* The results of the `p`'s, `x,,1,,` through `x,,n,,`, are returned as `NonEmptyList.of(x,,1,,, .., x,,n,,)`.
* If `p` or `sep` fails having consumed input, the whole parser fails. Requires at least
* one `p` to have been parsed.
*
* @example {{{
* scala> ...
* scala> val args = sepEndBy1(int, string(";\n"))
* scala> args.parse("7;\n3;\n2")
* val res0 = Success(NonEmptyList.of(7, 3, 2))
* scala> args.parse("")
* val res1 = Failure(..)
* scala> args.parse("1")
* val res2 = Success(NonEmptyList.of(1))
* scala> args.parse("1;\n2;\n")
* val res3 = Success(NonEmptyList.of(1, 2))
* }}}
*
* @param p the parser whose results are collected into a list.
* @param sep the delimiter that must be parsed between every `p`.
* @return a parser that parses `p` delimited by `sep`, returning the non-empty list of `p`'s results.
* @since 1.2.0
*/
def sepEndBy1[A](p: Parsley[A], sep: =>Parsley[_]): Parsley[NonEmptyList[A]] = parsley.combinator.sepEndBy1(p, sep).map { xxs =>
val (x::xs) = xxs
NonEmptyList(x, xs)
}

/** This combinator parses '''one''' or more occurrences of `p`, separated and ended by `sep`.
*
* Parses `p` followed by `sep` one or more times.
* The results of the `p`'s, `x,,1,,` through `x,,n,,`, are returned as `NonEmptyList.of(x,,1,,, .., x,,n,,)`.
* If `p` or `sep` fails having consumed input, the whole parser fails.
*
* @example {{{
* scala> ...
* scala> val args = endBy1(int, string(";\n"))
* scala> args.parse("7;\n3;\n2")
* val res0 = Failure(..)
* scala> args.parse("")
* val res1 = Failure(..)
* scala> args.parse("1;\n")
* val res2 = Success(NonEmptyList.of(1))
* scala> args.parse("1;\n2;\n")
* val res3 = Success(NonEmptyList.of(1, 2))
* }}}
*
* @param p the parser whose results are collected into a list.
* @param sep the delimiter that must be parsed between every `p`.
* @return a parser that parses `p` delimited by `sep`, returning the non-empty list of `p`'s results.
* @since 1.2.0
*/
def endBy1[A](p: Parsley[A], sep: =>Parsley[_]): Parsley[NonEmptyList[A]] = some(p <* sep)
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/* SPDX-FileCopyrightText: © 2023 Parsley Cats Contributors <https://github.com/j-mie6/parsley-cats/graphs/contributors>
* SPDX-License-Identifier: BSD-3-Clause
*/
package parsley.cats

import parsley.Parsley
Expand Down
Loading

0 comments on commit 3faf945

Please sign in to comment.