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

Ior syntax #1540

Merged
merged 19 commits into from
Apr 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
9 changes: 7 additions & 2 deletions core/src/main/scala/cats/data/Ior.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cats
package data

import cats.data.Validated.{Invalid, Valid}
import cats.functor.Bifunctor

import scala.annotation.tailrec

/** Represents a right-biased disjunction that is either an `A`, or a `B`, or both an `A` and a `B`.
Expand Down Expand Up @@ -47,6 +49,7 @@ sealed abstract class Ior[+A, +B] extends Product with Serializable {
final def unwrap: Either[Either[A, B], (A, B)] = fold(a => Left(Left(a)), b => Left(Right(b)), (a, b) => Right((a, b)))

final def toEither: Either[A, B] = fold(Left(_), Right(_), (_, b) => Right(b))
final def toValidated: Validated[A, B] = fold(Invalid(_), Valid(_), (_, b) => Valid(b))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

final def toOption: Option[B] = right
final def toList: List[B] = right.toList

Expand Down Expand Up @@ -102,7 +105,7 @@ sealed abstract class Ior[+A, +B] extends Product with Serializable {
fold(identity, ev, (_, b) => ev(b))

// scalastyle:off cyclomatic.complexity
final def append[AA >: A, BB >: B](that: AA Ior BB)(implicit AA: Semigroup[AA], BB: Semigroup[BB]): AA Ior BB = this match {
final def combine[AA >: A, BB >: B](that: AA Ior BB)(implicit AA: Semigroup[AA], BB: Semigroup[BB]): AA Ior BB = this match {
case Ior.Left(a1) => that match {
case Ior.Left(a2) => Ior.Left(AA.combine(a1, a2))
case Ior.Right(b2) => Ior.Both(a1, b2)
Expand Down Expand Up @@ -150,7 +153,7 @@ private[data] sealed abstract class IorInstances extends IorInstances0 {
}

implicit def catsDataSemigroupForIor[A: Semigroup, B: Semigroup]: Semigroup[Ior[A, B]] = new Semigroup[Ior[A, B]] {
def combine(x: Ior[A, B], y: Ior[A, B]) = x.append(y)
def combine(x: Ior[A, B], y: Ior[A, B]) = x.combine(y)
}

implicit def catsDataMonadForIor[A: Semigroup]: Monad[A Ior ?] = new Monad[A Ior ?] {
Expand Down Expand Up @@ -198,6 +201,8 @@ private[data] sealed trait IorFunctions {
def left[A, B](a: A): A Ior B = Ior.Left(a)
def right[A, B](b: B): A Ior B = Ior.Right(b)
def both[A, B](a: A, b: B): A Ior B = Ior.Both(a, b)
def leftNel[A, B](a: A): IorNel[A, B] = left(NonEmptyList.of(a))
def bothNel[A, B](a: A, b: B): IorNel[A, B] = both(NonEmptyList.of(a), b)

/**
* Create an `Ior` from two Options if at least one of them is defined.
Expand Down
14 changes: 12 additions & 2 deletions core/src/main/scala/cats/data/Validated.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ sealed abstract class Validated[+E, +A] extends Product with Serializable {
*/
def toOption: Option[A] = fold(_ => None, Some.apply)

/**
* Returns Valid values wrapped in Ior.Right, and None for Ior.Left values
*/
def toIor: Ior[E, A] = fold(Ior.left, Ior.right)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


/**
* Convert this value to a single element List if it is Valid,
* otherwise return an empty List
Expand Down Expand Up @@ -430,13 +435,18 @@ private[data] trait ValidatedFunctions {
}

/**
* Converts an `Either[A, B]` to an `Validated[A, B]`.
* Converts an `Either[A, B]` to a `Validated[A, B]`.
*/
def fromEither[A, B](e: Either[A, B]): Validated[A, B] = e.fold(invalid, valid)

/**
* Converts an `Option[B]` to an `Validated[A, B]`, where the provided `ifNone` values is returned on
* Converts an `Option[B]` to a `Validated[A, B]`, where the provided `ifNone` values is returned on
* the invalid of the `Validated` when the specified `Option` is `None`.
*/
def fromOption[A, B](o: Option[B], ifNone: => A): Validated[A, B] = o.fold(invalid[A, B](ifNone))(valid)

/**
* Converts an `Ior[A, B]` to a `Validated[A, B]`.
*/
def fromIor[A, B](ior: Ior[A, B]): Validated[A, B] = ior.fold(invalid, valid, (_, b) => valid(b))
}
1 change: 1 addition & 0 deletions core/src/main/scala/cats/data/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cats
package object data {
type NonEmptyStream[A] = OneAnd[Stream, A]
type ValidatedNel[+E, +A] = Validated[NonEmptyList[E], A]
type IorNel[+B, +A] = Ior[NonEmptyList[B], A]

def NonEmptyStream[A](head: A, tail: Stream[A] = Stream.empty): NonEmptyStream[A] =
OneAnd(head, tail)
Expand Down
1 change: 1 addition & 0 deletions core/src/main/scala/cats/syntax/all.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ trait AllSyntax
with FunctorFilterSyntax
with GroupSyntax
with InvariantSyntax
with IorSyntax
with ListSyntax
with MonadCombineSyntax
with MonadErrorSyntax
Expand Down
37 changes: 37 additions & 0 deletions core/src/main/scala/cats/syntax/ior.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cats.syntax

import cats.data.Ior

trait IorSyntax {
implicit def catsSyntaxIorId[A](a: A): IorIdOps[A] = new IorIdOps(a)
}

final class IorIdOps[A](val a: A) extends AnyVal {
/**
* Wrap a value in `Ior.Right`.
*
* Example:
* {{{
* scala> import cats.data.Ior
* scala> import cats.implicits._
*
* scala> "hello".rightIor[String]
* res0: Ior[String, String] = Right(hello)
* }}}
*/
def rightIor[B]: Ior[B, A] = Ior.right(a)
Copy link
Member

@ChristopherDavenport ChristopherDavenport Feb 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context in Either instances uses asRight so perhaps asRightIor. Depends on what sort of syntax maintainers would like to see but asRightIor makes more sense to me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only that way because of the Either.right projection ambiguity. The lack of such functions on Ior suggests to me that this way would be nicest.


/**
* Wrap a value in `Ior.Left`.
*
* Example:
* {{{
* scala> import cats.data.Ior
* scala> import cats.implicits._
*
* scala> "error".leftIor[String]
* res0: Ior[String, String] = Left(error)
* }}}
*/
def leftIor[B]: Ior[A, B] = Ior.left(a)
}
18 changes: 18 additions & 0 deletions core/src/main/scala/cats/syntax/list.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,23 @@ trait ListSyntax {
}

final class ListOps[A](val la: List[A]) extends AnyVal {

/**
* Returns an Option of NonEmptyList from a List
*
* Example:
* {{{
* scala> import cats.data.NonEmptyList
* scala> import cats.implicits._
*
* scala> val result1: List[Int] = List(1, 2)
* scala> result1.toNel
* res0: Option[NonEmptyList[Int]] = Some(NonEmptyList(1, 2))
*
* scala> val result2: List[Int] = List.empty[Int]
* scala> result2.toNel
* res1: Option[NonEmptyList[Int]] = None
* }}}
*/
def toNel: Option[NonEmptyList[A]] = NonEmptyList.fromList(la)
}
42 changes: 41 additions & 1 deletion core/src/main/scala/cats/syntax/option.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package cats
package syntax

import cats.data.{Validated, ValidatedNel}
import cats.data.{Ior, Validated, ValidatedNel}

trait OptionSyntax {
final def none[A]: Option[A] = Option.empty[A]
Expand Down Expand Up @@ -112,6 +112,46 @@ final class OptionOps[A](val oa: Option[A]) extends AnyVal {
*/
def toValidNel[B](b: => B): ValidatedNel[B, A] = oa.fold[ValidatedNel[B, A]](Validated.invalidNel(b))(Validated.Valid(_))

/**
* If the `Option` is a `Some`, return its value in a [[cats.data.Ior.Right]].
* If the `Option` is `None`, wrap the provided `B` value in a [[cats.data.Ior.Left]]
*
* Example:
* {{{
* scala> import cats.data.Ior
* scala> import cats.implicits._
*
* scala> val result1: Option[Int] = Some(3)
* scala> result1.toRightIor("error!")
* res0: Ior[String, Int] = Right(3)
*
* scala> val result2: Option[Int] = None
* scala> result2.toRightIor("error!")
* res1: Ior[String, Int] = Left(error!)
* }}}
*/
def toRightIor[B](b: => B): Ior[B, A] = oa.fold[Ior[B, A]](Ior.Left(b))(Ior.Right(_))

/**
* If the `Option` is a `Some`, return its value in a [[cats.data.Ior.Left]].
* If the `Option` is `None`, wrap the provided `B` value in a [[cats.data.Ior.Right]]
*
* Example:
* {{{
* scala> import cats.data.Ior
* scala> import cats.implicits._
*
* scala> val result1: Option[String] = Some("error!")
* scala> result1.toLeftIor(3)
* res0: Ior[String, Int] = Left(error!)
*
* scala> val result2: Option[String] = None
* scala> result2.toLeftIor(3)
* res1: Ior[String, Int] = Right(3)
* }}}
*/
def toLeftIor[B](b: => B): Ior[A, B] = oa.fold[Ior[A, B]](Ior.Right(b))(Ior.Left(_))

/**
* If the `Option` is a `Some`, return its value. If the `Option` is `None`,
* return the `empty` value for `Monoid[A]`.
Expand Down
1 change: 1 addition & 0 deletions core/src/main/scala/cats/syntax/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package object syntax {
object functorFilter extends FunctorFilterSyntax
object group extends GroupSyntax
object invariant extends InvariantSyntax
object ior extends IorSyntax
object list extends ListSyntax
object monadCombine extends MonadCombineSyntax
object monadError extends MonadErrorSyntax
Expand Down
30 changes: 24 additions & 6 deletions tests/src/test/scala/cats/tests/IorTests.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package cats
package tests

import cats.data.Ior
import cats.data.{Ior, NonEmptyList}
import cats.kernel.laws.GroupLaws
import cats.laws.discipline.{BifunctorTests, CartesianTests, MonadTests, SerializableTests, TraverseTests}
import cats.laws.discipline.arbitrary._
import cats.laws.discipline.{BifunctorTests, CartesianTests, MonadTests, SerializableTests, TraverseTests}
import org.scalacheck.Arbitrary._

class IorTests extends CatsSuite {
Expand Down Expand Up @@ -153,15 +153,15 @@ class IorTests extends CatsSuite {
}
}

test("append left") {
test("combine left") {
forAll { (i: Int Ior String, j: Int Ior String) =>
i.append(j).left should === (i.left.map(_ + j.left.getOrElse(0)).orElse(j.left))
i.combine(j).left should === (i.left.map(_ + j.left.getOrElse(0)).orElse(j.left))
}
}

test("append right") {
test("combine right") {
forAll { (i: Int Ior String, j: Int Ior String) =>
i.append(j).right should === (i.right.map(_ + j.right.getOrElse("")).orElse(j.right))
i.combine(j).right should === (i.right.map(_ + j.right.getOrElse("")).orElse(j.right))
}
}

Expand Down Expand Up @@ -198,6 +198,24 @@ class IorTests extends CatsSuite {
}
}

test("toValidated consistent with right") {
forAll { (x: Int Ior String) =>
x.toValidated.toOption should === (x.right)
}
}

test("leftNel") {
forAll { (x: String) =>
Ior.leftNel(x).left should === (Some(NonEmptyList.of(x)))
}
}

test("bothNel") {
forAll { (x: Int, y: String) =>
Ior.bothNel(y, x).onlyBoth should === (Some((NonEmptyList.of(y), x)))
}
}

test("getOrElse consistent with Option getOrElse") {
forAll { (x: Int Ior String, default: String) =>
x.getOrElse(default) should === (x.toOption.getOrElse(default))
Expand Down
22 changes: 17 additions & 5 deletions tests/src/test/scala/cats/tests/ValidatedTests.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package cats
package tests

import cats.data.{EitherT, NonEmptyList, Validated, ValidatedNel}
import cats.data.Validated.{Valid, Invalid}
import cats.laws.discipline.{BitraverseTests, TraverseTests, ApplicativeErrorTests, SerializableTests, CartesianTests}
import cats.data._
import cats.data.Validated.{Invalid, Valid}
import cats.laws.discipline.{ApplicativeErrorTests, BitraverseTests, CartesianTests, SerializableTests, TraverseTests}
import org.scalacheck.Arbitrary._
import cats.laws.discipline.{SemigroupKTests}
import cats.laws.discipline.SemigroupKTests
import cats.laws.discipline.arbitrary._
import cats.kernel.laws.{OrderLaws, GroupLaws}
import cats.kernel.laws.{GroupLaws, OrderLaws}

import scala.util.Try

Expand Down Expand Up @@ -187,6 +187,18 @@ class ValidatedTests extends CatsSuite {
}
}

test("fromIor consistent with Ior.toValidated"){
forAll { (i: Ior[String, Int]) =>
Validated.fromIor(i) should === (i.toValidated)
}
}

test("toIor then fromEither is identity") {
forAll { (v: Validated[String, Int]) =>
Validated.fromIor(v.toIor) should === (v)
}
}

test("isValid after combine, iff both are valid") {
forAll { (lhs: Validated[Int, String], rhs: Validated[Int, String]) =>
lhs.combine(rhs).isValid should === (lhs.isValid && rhs.isValid)
Expand Down