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 17 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
10 changes: 8 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,9 @@ 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 rightNel[A, B](b: B): IorNel[A, B] = right(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] = Ior.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
163 changes: 163 additions & 0 deletions core/src/main/scala/cats/syntax/ior.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package cats.syntax

import cats.data.{Ior, IorNel, NonEmptyList}

trait IorSyntax {
implicit def catsSyntaxIorId[A](a: A): IorIdOps[A] = new IorIdOps(a)
implicit def catsSyntaxListIorNel[A, B](list: List[IorNel[A, B]]): IorNelListOps[A, B] =
new IorNelListOps(list)
}

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)

/**
* Wrap a value in the right side of `Ior.Both`.
*
* Example:
* {{{
* scala> import cats.data.Ior
* scala> import cats.implicits._
*
* scala> "hello".putRightIor("error")
* res0: Ior[String, String] = Both(error,hello)
* }}}
*/
def putRightIor[B](left: B): Ior[B, A] = Ior.both(left, a)
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure about the necessity of these syntax:
putRightIor putLeftIor rightIorNel leftIorNel putRightIorNel putLeftIorNel
I am just not sure if the convenience of these methods overrides the cost of having them on everything.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kailuowang in this case, I wanted to offer in the syntax the put functions that helped build Both type just for convenience. I really found useful the invalidNel syntax of validated so I wanted to be able to use something similar when I was working with Ior, that is why I added rightIorNel-leftIorNel. This means that you also object to Ior.rightNel-Ior.leftNel functions on the companion object?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you are right, rightIorNel leftIorNel look fine to me now.


/**
* Wrap a value in the left side of `Ior.Both`.
*
* Example:
* {{{
* scala> import cats.data.Ior
* scala> import cats.implicits._
*
* scala> "error".putLeftIor("hello")
* res0: Ior[String, String] = Both(error,hello)
* }}}
*/
def putLeftIor[B](right: B): Ior[A, B] = Ior.both(a, right)

/**
* Wrap a value in a NonEmptyList in `Ior.Right`.
*
* Example:
* {{{
* scala> import cats.data.IorNel
* scala> import cats.implicits._
*
* scala> "hello".rightIorNel[String]
* res0: IorNel[String, String] = Right(hello)
* }}}
*/
def rightIorNel[B]: IorNel[B, A] = Ior.rightNel(a)

/**
* Wrap a value in a NonEmptyList in `Ior.Left`.
*
* Example:
* {{{
* scala> import cats.data.IorNel
* scala> import cats.implicits._
*
* scala> "error".leftIorNel[String]
* res0: IorNel[String, String] = Left(NonEmptyList(error))
* }}}
*/
def leftIorNel[B]: IorNel[A, B] = Ior.leftNel(a)

/**
* Wrap a value in a NonEmptyList in the right side of `Ior.Both`.
*
* Example:
* {{{
* scala> import cats.data.IorNel
* scala> import cats.implicits._
*
* scala> "hello".putRightIorNel[String]("error")
* res0: IorNel[String, String] = Both(NonEmptyList(error),hello)
* }}}
*/
def putRightIorNel[B](left: B): IorNel[B, A] = Ior.bothNel(left, a)

/**
* Wrap a value in a NonEmptyList in the left side of `Ior.Both`.
*
* Example:
* {{{
* scala> import cats.data.IorNel
* scala> import cats.implicits._
*
* scala> "I got it wrong".putLeftIorNel[String]("hello")
* res0: IorNel[String, String] = Both(NonEmptyList(I got it wrong),hello)
* }}}
*/
def putLeftIorNel[B](right: B): IorNel[A, B] = Ior.bothNel(a, right)
}


final class IorNelListOps[A, B](val list: List[IorNel[A, B]]) extends AnyVal {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that this can be further abstracted. I am also not sure having syntax for such a specific type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kailuowang you mean that those function can be added to the Ior datatype?

Copy link
Contributor

Choose a reason for hiding this comment

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

I just feel this thing is too specific,
F[G[A, B]] => G[H[A], H[B]]. if F is a Functor and Reducable I feel like that want to see this function on a type class or companion obect

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I am over thinking, maybe being able to reduce a list of Iors conveniently has good value. Generally speaking, I feel a good part of your PR can get easy consensus and this is a place I am less sure.


/**
* Returns single combined Ior by reducing a list of IorNel
*
* Example:
* {{{
* scala> import cats.data.Ior
* scala> import cats.data.NonEmptyList
* scala> import cats.implicits._
*
* scala> List("hello".rightIorNel[String], "error".leftIorNel[String]).reduceToIor
* res0: Ior[NonEmptyList[String], NonEmptyList[String]] = Both(NonEmptyList(error),NonEmptyList(hello))
* }}}
*/
def reduceToIor: Ior[NonEmptyList[A], NonEmptyList[B]] =
list.map(_.map(NonEmptyList(_, Nil))).reduce(_ combine _)

/**
* Returns an Option of a single combined Ior by reducing a list of IorNel
*
* Example:
* {{{
* scala> import cats.data.Ior
* scala> import cats.data.IorNel
* scala> import cats.data.NonEmptyList
* scala> import cats.implicits._
*
* scala> List("hello".rightIorNel[String], "error".leftIorNel[String]).reduceToOptionIor
* res0: Option[Ior[NonEmptyList[String], NonEmptyList[String]]] = Some(Both(NonEmptyList(error),NonEmptyList(hello)))
*
* scala> List.empty[IorNel[String, String]].reduceToOptionIor
* res1: Option[Ior[NonEmptyList[String], NonEmptyList[String]]] = None
* }}}
*/
def reduceToOptionIor: Option[Ior[NonEmptyList[A], NonEmptyList[B]]] =
list.map(_.map(NonEmptyList(_, Nil))) reduceOption (_ combine _)
}
61 changes: 60 additions & 1 deletion core/src/main/scala/cats/syntax/list.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,71 @@
package cats
package syntax

import cats.data.NonEmptyList
import cats.data.Validated.{Invalid, Valid}
import cats.data.{Ior, IorNel, NonEmptyList, ValidatedNel}

trait ListSyntax {
implicit def catsSyntaxList[A](la: List[A]): ListOps[A] = new ListOps(la)
}

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)

/**
* Returns a IorNel from a List
*
* Example:
* {{{
* scala> import cats.data.IorNel
* scala> import cats.implicits._
*
* scala> val result1: List[String] = List("error 1", "error 2")
* scala> result1.toIorNel(1)
* res0: IorNel[String, Int] = Left(NonEmptyList(error 1, error 2))
*
* scala> val result2: List[String] = List.empty[String]
* scala> result2.toIorNel(1)
* res1: IorNel[String, Int] = Right(1)
* }}}
*/
def toIorNel[B](ifEmpty: => B): IorNel[A, B] =
Copy link
Contributor

Choose a reason for hiding this comment

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

The intention of this isn't very clear. I mean if you read a code result1.toIorNel(1), it's not obvious that the result1 is the error list while 1 is the valid result. Users will have to read the code here to understand the intention. I am not sure about such syntax addition.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kailuowang you are right, I didn't make it clear. This is motivated due to a use case I find frequently. If there is some validations made and were returned as a List, meaning that if it is empty everything went well and if it is not then there are errors present, I had to do something like this:

errorList match {
  case Nil => Valid("this is valid")
  case err :: errs => Invalid(NoneEmptyList(err, errs))
}

I found convenient to have a syntax that helped me with this for ValidatedNel and IorNel.

I do agree with you that if there is not consensus on this I can roll it back and maybe try it again on another pull request when I can make an example illustrating the use of what I proposed. I'll wait for your thoughts on this.

toNel.fold[IorNel[A, B]](Ior.rightNel(ifEmpty))(Ior.left)

/**
* Returns a ValidatedNel from a List
*
* Example:
* {{{
* scala> import cats.data.ValidatedNel
* scala> import cats.implicits._
*
* scala> val result1: List[String] = List("error 1", "error 2")
* scala> result1.toValidatedNel(1)
* res0: ValidatedNel[String, Int] = Invalid(NonEmptyList(error 1, error 2))
*
* scala> val result2: List[String] = List.empty[String]
* scala> result2.toValidatedNel(1)
* res1: ValidatedNel[String, Int] = Valid(1)
* }}}
*/
def toValidatedNel[B](ifEmpty: => B): ValidatedNel[A, B] =
toNel.fold[ValidatedNel[A, B]](Valid(ifEmpty))(Invalid(_))
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above.

}
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
Loading