From 14bd1f239c6731e02f11fdb7d29c66f7b74e2419 Mon Sep 17 00:00:00 2001 From: Antonio Gelameris Date: Sat, 25 Jun 2022 22:58:44 +0200 Subject: [PATCH 1/5] Add inconsistency proof for a Validated Monad in Documentation --- docs/datatypes/validated.md | 60 +++++++++++++++++++++++++++++++++--- docs/typeclasses/parallel.md | 2 +- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/docs/datatypes/validated.md b/docs/datatypes/validated.md index c4a496f08d..d1b0c4b614 100644 --- a/docs/datatypes/validated.md +++ b/docs/datatypes/validated.md @@ -589,8 +589,62 @@ validatedMonad.tuple2(Validated.invalidNec[String, Int]("oops"), Validated.inval ``` This one short circuits! Therefore, if we were to define a `Monad` (or `FlatMap`) instance for `Validated` we would -have to override `ap` to get the behavior we want. But then the behavior of `flatMap` would be inconsistent with -that of `ap`, not good. Therefore, `Validated` has only an `Applicative` instance. +have to override `ap` to get the behavior we want. + +```scala mdoc:silent:nest +import cats.Monad + +implicit def inconsistentValidatedMonad[E: Semigroup]: Monad[Validated[E, *]] = + new Monad[Validated[E, *]] { + def flatMap[A, B](fa: Validated[E, A])(f: A => Validated[E, B]): Validated[E, B] = + fa match { + case Valid(a) => f(a) + case i@Invalid(_) => i + } + + def pure[A](x: A): Validated[E, A] = Valid(x) + + @annotation.tailrec + def tailRecM[A, B](a: A)(f: A => Validated[E, Either[A, B]]): Validated[E, B] = + f(a) match { + case Valid(Right(b)) => Valid(b) + case Valid(Left(a)) => tailRecM(a)(f) + case i@Invalid(_) => i + } + + override def ap[A, B](f: Validated[E, A => B])(fa: Validated[E, A]): Validated[E, B] = + (fa, f) match { + case (Valid(a), Valid(fab)) => Valid(fab(a)) + case (i@Invalid(_), Valid(_)) => i + case (Valid(_), i@Invalid(_)) => i + case (Invalid(e1), Invalid(e2)) => Invalid(Semigroup[E].combine(e1, e2)) + } + } +``` + +But then the behavior of `flatMap` would be inconsistent with that of `ap`, and this will violate one of the [FlatMap laws](https://github.com/typelevel/cats/blob/main/laws/src/main/scala/cats/laws/FlatMapLaws.scala), `flatMapConsistentApply`: + +```scala +def flatMapConsistentApply[A, B](fa: F[A], fab: F[A => B]): IsEq[F[B]] = + fab.ap(fa) <-> fab.flatMap(f => fa.map(f)) +``` + +```scala mdoc:silent +import cats.laws._ + +val inconsistentFlatMapLawsForValidated = + FlatMapLaws[Validated[NonEmptyChain[String], *]](inconsistentValidatedMonad) + +val fa = Validated.invalidNec[String, Int]("oops") +val fb = (i:Int) => Validated.invalidNec[String, Double](s"$i") +val fab = Validated.invalidNec[String, Int => Double]("Broken function") +``` + +```scala mdoc +inconsistentFlatMapLawsForValidated.flatMapConsistentApply(fa , fab) +``` + +Therefore, `Validated` has only an `Applicative` instance. ## `Validated` vs `Either` @@ -618,8 +672,6 @@ val houseNumber = config.parse[Int]("house_number").andThen{ n => The `withEither` method allows you to temporarily turn a `Validated` instance into an `Either` instance and apply it to a function. ```scala mdoc:silent -import cats.implicits._ // get Either#flatMap - def positive(field: String, i: Int): Either[ConfigError, Int] = { if (i >= 0) Right(i) else Left(ParseError(field)) diff --git a/docs/typeclasses/parallel.md b/docs/typeclasses/parallel.md index 52b618b9b9..659fe542cf 100644 --- a/docs/typeclasses/parallel.md +++ b/docs/typeclasses/parallel.md @@ -4,7 +4,7 @@ When browsing the various `Monads` included in Cats, you may have noticed that some of them have data types that are actually of the same structure, but instead have instances of `Applicative`. E.g. `Either` and `Validated`. -This is because defining a `Monad` instance for data types like `Validated` would be inconsistent with its error-accumulating behaviour. +This is because defining a `Monad` instance for data types like `Validated` [would be inconsistent](../datatypes/validated.md#of-flatmaps-and-eithers) with its error-accumulating behaviour. In short, `Monads` describe dependent computations and `Applicatives` describe independent computations. Sometimes however, we want to use both in conjunction with each other. From 7d72164405e0daff0c0cb5e9bc7eebe6c2d95466 Mon Sep 17 00:00:00 2001 From: Antonio Gelameris Date: Sat, 25 Jun 2022 23:04:49 +0200 Subject: [PATCH 2/5] Removed useless variable --- docs/datatypes/validated.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/datatypes/validated.md b/docs/datatypes/validated.md index d1b0c4b614..0278f21903 100644 --- a/docs/datatypes/validated.md +++ b/docs/datatypes/validated.md @@ -636,7 +636,6 @@ val inconsistentFlatMapLawsForValidated = FlatMapLaws[Validated[NonEmptyChain[String], *]](inconsistentValidatedMonad) val fa = Validated.invalidNec[String, Int]("oops") -val fb = (i:Int) => Validated.invalidNec[String, Double](s"$i") val fab = Validated.invalidNec[String, Int => Double]("Broken function") ``` From cafb91a38b032b47495e0ee3b06fe2a44fead31d Mon Sep 17 00:00:00 2001 From: Antonio Gelameris Date: Sun, 26 Jun 2022 00:49:16 +0200 Subject: [PATCH 3/5] Applying PR Suggestions --- docs/datatypes/validated.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/datatypes/validated.md b/docs/datatypes/validated.md index 0278f21903..69e97adc2e 100644 --- a/docs/datatypes/validated.md +++ b/docs/datatypes/validated.md @@ -594,7 +594,7 @@ have to override `ap` to get the behavior we want. ```scala mdoc:silent:nest import cats.Monad -implicit def inconsistentValidatedMonad[E: Semigroup]: Monad[Validated[E, *]] = +implicit def accumulatingValidatedMonad[E: Semigroup]: Monad[Validated[E, *]] = new Monad[Validated[E, *]] { def flatMap[A, B](fa: Validated[E, A])(f: A => Validated[E, B]): Validated[E, B] = fa match { @@ -625,6 +625,8 @@ implicit def inconsistentValidatedMonad[E: Semigroup]: Monad[Validated[E, *]] = But then the behavior of `flatMap` would be inconsistent with that of `ap`, and this will violate one of the [FlatMap laws](https://github.com/typelevel/cats/blob/main/laws/src/main/scala/cats/laws/FlatMapLaws.scala), `flatMapConsistentApply`: ```scala +// the `<->` operator means "is equivalent to" and returns a data structure +// that can be used to prove the equivalence of the two expressions def flatMapConsistentApply[A, B](fa: F[A], fab: F[A => B]): IsEq[F[B]] = fab.ap(fa) <-> fab.flatMap(f => fa.map(f)) ``` @@ -633,7 +635,7 @@ def flatMapConsistentApply[A, B](fa: F[A], fab: F[A => B]): IsEq[F[B]] = import cats.laws._ val inconsistentFlatMapLawsForValidated = - FlatMapLaws[Validated[NonEmptyChain[String], *]](inconsistentValidatedMonad) + FlatMapLaws[Validated[NonEmptyChain[String], *]](accumulatingValidatedMonad) val fa = Validated.invalidNec[String, Int]("oops") val fab = Validated.invalidNec[String, Int => Double]("Broken function") From 6c990e2904f87c3394b8df7dd810665daf6fde3f Mon Sep 17 00:00:00 2001 From: Antonio Gelameris Date: Sun, 26 Jun 2022 22:26:46 +0200 Subject: [PATCH 4/5] Adding pr suggestions --- docs/datatypes/validated.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/datatypes/validated.md b/docs/datatypes/validated.md index 69e97adc2e..a710db61be 100644 --- a/docs/datatypes/validated.md +++ b/docs/datatypes/validated.md @@ -626,7 +626,7 @@ But then the behavior of `flatMap` would be inconsistent with that of `ap`, and ```scala // the `<->` operator means "is equivalent to" and returns a data structure -// that can be used to prove the equivalence of the two expressions +// `IsEq` that is used to verify the equivalence of the two expressions def flatMapConsistentApply[A, B](fa: F[A], fab: F[A => B]): IsEq[F[B]] = fab.ap(fa) <-> fab.flatMap(f => fa.map(f)) ``` From f4216ddd74b612a6b04559bc3ad9cafecf097946 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 1 Jul 2022 15:41:06 -0700 Subject: [PATCH 5/5] Tweak --- docs/datatypes/validated.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/datatypes/validated.md b/docs/datatypes/validated.md index a710db61be..1a7d982e8a 100644 --- a/docs/datatypes/validated.md +++ b/docs/datatypes/validated.md @@ -634,7 +634,7 @@ def flatMapConsistentApply[A, B](fa: F[A], fab: F[A => B]): IsEq[F[B]] = ```scala mdoc:silent import cats.laws._ -val inconsistentFlatMapLawsForValidated = +val flatMapLawsForAccumulatingValidatedMonad = FlatMapLaws[Validated[NonEmptyChain[String], *]](accumulatingValidatedMonad) val fa = Validated.invalidNec[String, Int]("oops") @@ -642,7 +642,7 @@ val fab = Validated.invalidNec[String, Int => Double]("Broken function") ``` ```scala mdoc -inconsistentFlatMapLawsForValidated.flatMapConsistentApply(fa , fab) +flatMapLawsForAccumulatingValidatedMonad.flatMapConsistentApply(fa , fab) ``` Therefore, `Validated` has only an `Applicative` instance.