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 EitherT and IorT constructors from Option with monadic default #3426

Merged
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
9 changes: 9 additions & 0 deletions core/src/main/scala/cats/data/EitherT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,15 @@ object EitherT extends EitherTInstances {
}
)

/** Similar to `fromOptionF` but the left is carried from monadic `F[_]` context when the option is `None` */
final def fromOptionM[F[_], E, A](fopt: F[Option[A]], ifNone: => F[E])(implicit F: Monad[F]): EitherT[F, E, A] =
EitherT(
F.flatMap(fopt) {
case Some(a) => F.pure(Right.apply[E, A](a))
case None => F.map(ifNone)(Left.apply[E, A])
}
)

/** If the condition is satisfied, return the given `A` in `Right`
* lifted into the specified `Applicative`, otherwise, return the
* given `E` in `Left` lifted into the specified `Applicative`.
Expand Down
9 changes: 9 additions & 0 deletions core/src/main/scala/cats/data/IorT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,15 @@ object IorT extends IorTInstances {
final def fromOptionF[F[_], E, A](foption: F[Option[A]], ifNone: => E)(implicit F: Functor[F]): IorT[F, E, A] =
IorT(F.map(foption)(_.fold[Ior[E, A]](Ior.left(ifNone))(Ior.right)))

/** Similar to `fromOptionF` but the left is carried from monadic `F[_]` context when the option is `None` */
final def fromOptionM[F[_], E, A](foption: F[Option[A]], ifNone: => F[E])(implicit F: Monad[F]): IorT[F, E, A] =
IorT(
F.flatMap(foption) {
case Some(a) => F.pure(Ior.right[E, A](a))
case None => F.map(ifNone)(Ior.left[E, A])
}
)

/**
* Uses the [[http://typelevel.org/cats/guidelines.html#partially-applied-type-params Partially Applied Type Params technique]] for ergonomics.
*/
Expand Down
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/data/OptionT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,15 @@ final case class OptionT[F[_], A](value: F[Option[A]]) {
def toRight[L](left: => L)(implicit F: Functor[F]): EitherT[F, L, A] =
EitherT(cata(Left(left), Right.apply))

def toRightF[L](left: => F[L])(implicit F: Monad[F]): EitherT[F, L, A] =
EitherT(cataF(F.map(left)(Left.apply[L, A]), a => F.pure(Right(a))))

def toLeft[R](right: => R)(implicit F: Functor[F]): EitherT[F, A, R] =
EitherT(cata(Right(right), Left.apply))

def toLeftF[R](right: => F[R])(implicit F: Monad[F]): EitherT[F, A, R] =
EitherT(cataF(F.map(right)(Right.apply[A, R]), a => F.pure(Left(a))))

def show(implicit F: Show[F[Option[A]]]): String = F.show(value)

def compare(that: OptionT[F, A])(implicit o: Order[F[Option[A]]]): Int =
Expand Down
3 changes: 2 additions & 1 deletion docs/src/main/tut/datatypes/eithert.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,15 @@ val numberFET: EitherT[List, String, Int] = EitherT(numberFE)

An `Option[B]` or an `F[Option[B]]`, along with a default value, can be passed to
`EitherT.fromOption` and `EitherT.fromOptionF`, respectively, to produce an
`EitherT`.
`EitherT`. For `F[Option[B]]` and default `F[A]`, there is `EitherT.fromOptionM`.

```tut:book
val myOption: Option[Int] = None
val myOptionList: List[Option[Int]] = List(None, Some(2), Some(3), None, Some(5))

val myOptionET = EitherT.fromOption[Future](myOption, "option not defined")
val myOptionListET = EitherT.fromOptionF(myOptionList, "option not defined")
val myOptionListETM = EitherT.fromOptionM(myOptionList, List("option not defined"))
```

## From `ApplicativeError[F, E]` to `EitherT[F, E, A]`
Expand Down
3 changes: 2 additions & 1 deletion docs/src/main/tut/datatypes/iort.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,14 +223,15 @@ val numberF: IorT[Option, String, Int] = IorT.fromEitherF(numberFEither)

An `Option[B]` or an `F[Option[B]]`, along with a default value, can be passed
to `IorT.fromOption` and `IorT.fromOptionF`, respectively, to produce an
`IorT`.
`IorT`. For `F[Option[B]]` and default `F[A]`, there is `IorT.fromOptionM`.

```tut:silent
val numberOption: Option[Int] = None
val numberFOption: List[Option[Int]] = List(None, Some(2), None, Some(5))

val number = IorT.fromOption[List](numberOption, "Not defined")
val numberF = IorT.fromOptionF(numberFOption, "Not defined")
val numberM = IorT.fromOptionM(numberFOption, List("Not defined"))
```

## Creating an `IorT[F, A, B]` from a `Boolean` test
Expand Down
1 change: 1 addition & 0 deletions docs/src/main/tut/jump_start_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ def ensureUserExists(userId: Int): EitherT[Future, BaseException, User] = {

`toRight` is pretty analogous to the method `Either.fromOption` mentioned before: just as `fromOption` built an `Either` from an `Option`, `toRight` creates an `EitherT` from an `OptionT`.
If the original `OptionT` stores `Some` value, it will be wrapped into `Right`; otherwise the value provided as the `left` parameter will be wrapped into a `Left`.
To provide the `left` value within the monad, there is corresponding `toRightF` method.

`toLeft` is `toRight`'s counterpart which wraps the `Some` value into `Left` and transforms `None` into `Right` enclosing the provided `right` value.
This is less commonly used in practice, but can serve e.g. for enforcing uniqueness checks in code.
Expand Down
1 change: 1 addition & 0 deletions docs/src/main/tut/nomenclature.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ Here, we use `ET` to abbreviate `EitherT`; and we use `A` and `B` as type variab
| `Either[A, B] => ET[F, A, B]` | `fromEither` | `F: Applicative` |
| `Option[B] => A => ET[F, A, B]` | `fromOption` | `F: Applicative` |
| `F[Option[B]] => A => ET[F, A, B]` | `fromOptionF` | `F: Functor` |
| `F[Option[B]] => F[A] => ET[F, A, B]` | `fromOptionM` | `F: Monad` |
| `Boolean => B => A => ET[F, A, B]` | `cond` | `F: Applicative` |
| `ET[F, A, B] => (A => C) => (B => C) => F[C]` | `fold` | `F: Functor` |
| `ET[F, A, B] => ET[F, B, A]` | `swap` | `F: Functor` |
Expand Down
12 changes: 12 additions & 0 deletions tests/src/test/scala/cats/tests/EitherTSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,18 @@ class EitherTSuite extends CatsSuite {
}
}

test("fromOptionF isLeft consistent with Option isEmpty") {
forAll { (option: Option[Int], s: String) =>
EitherT.fromOptionF[Id, String, Int](option, s).isLeft should ===(option.isEmpty)
}
}

test("fromOptionM consistent with fromOptionF") {
forAll { (option: Option[Int], s: String) =>
EitherT.fromOptionM[Id, String, Int](option, s) should ===(EitherT.fromOptionF[Id, String, Int](option, s))
}
}

test("cond consistent with Either.cond") {
forAll { (cond: Boolean, s: String, i: Int) =>
EitherT.cond[Id](cond, s, i).value should ===(Either.cond(cond, s, i))
Expand Down
6 changes: 6 additions & 0 deletions tests/src/test/scala/cats/tests/IorTSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,12 @@ class IorTSuite extends CatsSuite {
}
}

test("IorT.fromOptionM consistent with IorT.fromOptionF") {
forAll { (option: Option[Int], s: String) =>
IorT.fromOptionM[Id, String, Int](option, s) should ===(IorT.fromOptionF[Id, String, Int](option, s))
}
}

test("IorT.cond isRight equals test") {
forAll { (test: Boolean, s: String, i: Int) =>
val iort = IorT.cond[Id](test, s, i)
Expand Down
12 changes: 12 additions & 0 deletions tests/src/test/scala/cats/tests/OptionTSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -387,12 +387,24 @@ class OptionTSuite extends CatsSuite {
}
}

test("toRight and toRightF consistent") {
forAll { (o: OptionT[List, Int], s: String) =>
o.toRight(s) should ===(o.toRightF(List(s)))
}
}

test("toLeft consistent with isDefined") {
forAll { (o: OptionT[List, Int], s: String) =>
o.toLeft(s).isLeft should ===(o.isDefined)
}
}

test("toLeft and toLeftF consistent") {
forAll { (o: OptionT[List, Int], s: String) =>
o.toLeft(s) should ===(o.toLeftF(List(s)))
}
}

test("isDefined is negation of isEmpty") {
forAll { (o: OptionT[List, Int]) =>
o.isDefined should ===(o.isEmpty.map(!_))
Expand Down