Skip to content

Commit

Permalink
Added extra methods to Result (#23)
Browse files Browse the repository at this point in the history
* Added extra methods to Result

* Added explicit types

* Fixed type of get

* Removed TODO

* Started on tests, added toEither

* More tests

* All tests complete
  • Loading branch information
j-mie6 authored Jan 6, 2021
1 parent 1c64f5f commit 7533b66
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 7 deletions.
139 changes: 132 additions & 7 deletions src/main/scala/parsley/Result.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,137 @@
package parsley

import scala.util.{Try, Success => TSuccess, Failure => TFailure}

/**
* Result of a parser. Either a `Success[A]` or a `Failure`
* @tparam A The type of expected success result
*/
//TODO: Make these a bit more full fledged
sealed abstract class Result[+A]
{
def toOption: Option[A]
def toEither: Either[String, A]
/** Applies `fa` if this is a `Failure` or `fb` if this is a `Success`.
*
* @param ferr the function to apply if this is a `Failure`
* @param fa the function to apply if this is a `Success`
* @return the results of applying the function
*/
def fold[B](ferr: String => B, fa: A => B): B = this match {
case Success(x) => fa(x)
case Failure(msg) => ferr(msg)
}

/** Executes the given side-effecting function if this is a `Success`.
*
* @param f The side-effecting function to execute.
*/
def foreach[U](f: A => U): Unit = this match {
case Success(x) => f(x)
case _ =>
}

/** Returns the results's value.
*
* @note The result must not be a failure.
* @throws NoSuchElementException if the result is a failure.
*/
def get: A

/** Returns the value from this `Success` or the given argument if this is a `Failure`. */
def getOrElse[B >: A](or: =>B): B = orElse(Success(or)).get

/** Returns this `Success` or the given argument if this is a `Failure`. */
def orElse[B >: A](or: =>Result[B]): Result[B] = this match {
case Success(_) => this
case _ => or
}

/** Returns `true` if this is a `Success` and its value is equal to `elem` (as determined by `==`),
* returns `false` otherwise.
*
* @param elem the element to test.
* @return `true` if this is a `Success` value equal to `elem`.
*/
final def contains[B >: A](elem: B): Boolean = exists(_ == elem)

/** Returns `true` if `Failure` or returns the result of the application of
* the given predicate to the `Success` value.
*/
def forall(f: A => Boolean): Boolean = this match {
case Success(x) => f(x)
case _ => true
}

/** Returns `false` if `Failure` or returns the result of the application of
* the given predicate to the `Success` value.
*/
def exists(p: A => Boolean): Boolean = this match {
case Success(x) => p(x)
case _ => false
}

/** Binds the given function across `Success`.
*
* @param f The function to bind across `Success`.
*/
def flatMap[B](f: A => Result[B]): Result[B] = this match {
case Success(x) => f(x)
case _ => this.asInstanceOf[Result[B]]
}

/** Returns the right value if this is right
* or this value if this is left
*
* Equivalent to `flatMap(id => id)`
*/
def flatten[B](implicit ev: A <:< Result[B]): Result[B] = flatMap(ev)

/** The given function is applied if this is a `Success`. */
def map[B](f: A => B): Result[B] = this match {
case Success(x) => Success(f(x))
case _ => this.asInstanceOf[Result[B]]
}

/** Returns `Success` with the existing value of `Success` if this is a `Success`
* and the given predicate `p` holds for the right value,
* or `Failure(msg)` if this is a `Success` and the given predicate `p` does not hold for the right value,
* or `Failure` with the existing value of `Failure` if this is a `Failure`.
*/
def filterOrElse(p: A => Boolean, msg: =>String): Result[A] = this match {
case Success(x) if !p(x) => Failure(msg)
case _ => this
}

/** Returns a `Seq` containing the `Success` value if
* it exists or an empty `Seq` if this is a `Failure`.
*/
def toSeq: collection.immutable.Seq[A] = this match {
case Success(x) => collection.immutable.Seq(x)
case _ => collection.immutable.Seq.empty
}

/** Returns a `Some` containing the `Success` value
* if it exists or a `None` if this is a `Failure`.
*/
def toOption: Option[A] = this match {
case Success(x) => Some(x)
case _ => None
}

/** Converts the `Result` into a `Try` where `Failure` maps to a plain `Exception` */
def toTry: Try[A] = this match {
case Success(x) => TSuccess(x)
case Failure(msg) => TFailure(new Exception(s"ParseError: $msg"))
}

def toEither: Either[String, A] = this match {
case Success(x) => Right(x)
case Failure(msg) => Left(msg)
}

/** Returns `true` if this is a `Success`, `false` otherwise. */
def isSuccess: Boolean

/** Returns `true` if this is a `Failure`, `false` otherwise. */
def isFailure: Boolean
}

/**
Expand All @@ -18,8 +141,9 @@ sealed abstract class Result[+A]
*/
case class Success[A] private [parsley] (x: A) extends Result[A]
{
override def toOption: Option[A] = Some(x)
override def toEither: Either[String, A] = Right(x)
override def isSuccess: Boolean = true
override def isFailure: Boolean = false
override def get: A = x
}

/**
Expand All @@ -28,6 +152,7 @@ case class Success[A] private [parsley] (x: A) extends Result[A]
*/
case class Failure private [parsley] (msg: String) extends Result[Nothing]
{
override def toOption: Option[Nothing] = None
override def toEither: Either[String, Nothing] = Left(msg)
override def isSuccess: Boolean = false
override def isFailure: Boolean = true
override def get: Nothing = throw new NoSuchElementException("get called on Failure")
}
102 changes: 102 additions & 0 deletions src/test/scala/parsley/ResultTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package parsley

import scala.language.implicitConversions

class ResultTests extends ParsleyTest {
"Success" should "return true for isSuccess" in {
Success(7).isSuccess shouldBe true
}
it should "return false for isFailure" in {
Success(7).isFailure shouldBe false
}
it should "not throw an exception on get" in {
noException should be thrownBy (Success(4).get)
}

"Failure" should "return false for isSuccess" in {
Failure("oops").isSuccess shouldBe false
}
it should "return true for isFailure" in {
Failure("oops").isFailure shouldBe true
}
it should "throw an exception on get" in {
a [NoSuchElementException] should be thrownBy (Failure("oops").get)
}

"Result[A]" should "behave like Either[String, A] on success" in {
val rx = for {
y <- Success(5)
x <- Success(y + 4)
} yield x
val ex = for {
y <- Right(5)
x <- Right(y + 4)
} yield x
rx.toEither should equal (ex)
}
it should "behave like Either[String, A] on failure" in {
val rx = for {
y <- Failure("oops")
x <- Success((y: Int) + 4)
} yield x
val ex = for {
y <- Left("oops")
x <- Right((y: Int) + 4)
} yield x
rx.toEither should equal (ex)
val rx2 = for {
y <- Success(5)
x <- Failure("oops")
} yield x
val ex2 = for {
y <- Right(5)
x <- Left("oops")
} yield x
rx2.toEither should equal (ex2)
}
it should "behave like Option[A] when extracting elements" in {
Success(7).getOrElse(5) should equal (Some(7).getOrElse(5))
Failure("oops").getOrElse(5) should equal (None.getOrElse(5))
Success(7).toOption should equal (Some(7))
Failure("oops").toOption should equal (None)
}
it should "throw an exception when it fails and converted to Try" in {
noException should be thrownBy (Success(7).toTry.get)
an [Exception] should be thrownBy (Failure("oops").toTry.get)
}
it should "be convertible to a possibly one element sequence" in {
Success(4).toSeq should have size 1
Failure("oops").toSeq shouldBe empty
}
it should "support contains" in {
Success(5).contains(5) shouldBe true
Success(4).contains(5) shouldBe false
Failure("oops").contains(5) shouldBe false
}
it should "support forall" in {
Success(5).forall((x: Int) => x % 2 == 0) shouldBe false
Success(4).forall((x: Int) => x % 2 == 0) shouldBe true
Failure("msg").forall((x: Int) => x % 2 == 0) shouldBe true
}
it should "support foreach" in {
var x = 0
for (y <- Failure("msg")) x = y
x shouldBe 0
for (y <- Success(3)) x = y
x shouldBe 3
}
it should "flatten correctly" in {
Success(Success(4)).flatten.isSuccess shouldBe true
Success(Failure("ops")).flatten.isFailure shouldBe true
Failure("msg").flatten.isFailure shouldBe true
}
it should "be filterable" in {
Success(5).filterOrElse(_ == 5, "???") shouldBe Success(5)
Success(5).filterOrElse(_ == 4, "should be 4!") shouldBe Failure("should be 4!")
Failure("oops").filterOrElse(_ == 4, "should be 4!") shouldBe Failure("oops")
}
it should "be foldable" in {
Success(4).fold(_ => 0, _+1) shouldBe 5
Failure("oops").fold(_ => 0, (x: Int) => x + 1) shouldBe 0
}
}

0 comments on commit 7533b66

Please sign in to comment.