diff --git a/docs/src/main/resources/microsite/data/menu.yml b/docs/src/main/resources/microsite/data/menu.yml index 185ed4341c..b889b89435 100644 --- a/docs/src/main/resources/microsite/data/menu.yml +++ b/docs/src/main/resources/microsite/data/menu.yml @@ -128,6 +128,10 @@ options: url: datatypes/id.html menu_type: data + - title: Ior + url: datatypes/ior.html + menu_type: data + - title: Kleisli url: datatypes/kleisli.html menu_type: data diff --git a/docs/src/main/tut/datatypes/ior.md b/docs/src/main/tut/datatypes/ior.md new file mode 100644 index 0000000000..82fb808bac --- /dev/null +++ b/docs/src/main/tut/datatypes/ior.md @@ -0,0 +1,126 @@ +--- +layout: docs +title: "Ior" +section: "data" +source: "core/src/main/scala/cats/data/Ior.scala" +scaladoc: "#cats.data.Ior" +--- +# Ior + +`Ior` represents an inclusive-or relationship between two data types. +This makes it very similar to the [`Either`](either.html) data type, which represents an "exclusive-or" relationship. +What this means, is that an `Ior[A, B]` (also written as `A Ior B`) can contain either an `A`, a `B`, or both an `A` and `B`. +Another similarity to `Either` is that `Ior` is right-biased, +which means that the `map` and `flatMap` functions will work on the right side of the `Ior`, in our case the `B` value. +You can see this in the function signature of `map`: + +```scala +def map[B, C](fa: A Ior B)(f: B => C): A Ior C +``` + +We can create `Ior` values using `Ior.left`, `Ior.right` and `Ior.both`: + +```tut +import cats.data._ + +val right = Ior.right[String, Int](3) + +val left = Ior.left[String, Int]("Error") + +val both = Ior.both("Warning", 3) +``` + +Cats also offers syntax enrichment for `Ior`. The `leftIor` and `rightIor` functions can be imported from `cats.syntax.ior._`: + +```tut +import cats.syntax.ior._ + +val right = 3.rightIor + +val left = "Error".leftIor +``` + + +When we look at the `Monad` or `Applicative` instances of `Ior`, we can see that they actually requires a `Semigroup` instance on the left side. +This is because `Ior` will actually accumulate failures on the left side, very similar to how the [`Validated`](validated.html) data type does. +This means we can accumulate data on the left side while also being able to short-circuit upon the first right-side-only value. +For example, sometimes, we might want to accumulate warnings together with a valid result and only halt the computation on a "hard error" +Here's an example of how we might be able to do that: + +```tut:silent +import cats.implicits._ +import cats.data.{ NonEmptyList => Nel } + +type Failures = Nel[String] + +case class Username(value: String) extends AnyVal +case class Password(value: String) extends AnyVal + +case class User(name: Username, pw: Password) + +def validateUsername(u: String): Failures Ior Username = { + if (u.isEmpty) + Nel.one("Can't be empty").leftIor + else if (u.contains(".")) + Ior.both(Nel.one("Dot in name is deprecated"), Username(u)) + else + Username(u).rightIor +} + +def validatePassword(p: String): Failures Ior Password = { + if (p.length < 8) + Nel.one("Password too short").leftIor + else if (p.length < 10) + Ior.both(Nel.one("Password should be longer"), Password(p)) + else + Password(p).rightIor +} + +def validateUser(name: String, password: String): Failures Ior User = + (validateUsername(name), validatePassword(password)).mapN(User) + +``` + +Now we're able to validate user data and also accumulate non-fatal warnings: + +```tut + +validateUser("John", "password12") + +validateUser("john.doe", "password") + +validateUser("jane", "short") + +``` + +To extract the values, we can use the `fold` method, which expects a function for each case the `Ior` can represent: + +```tut + +validateUser("john.doe", "password").fold( + errorNel => s"Error: ${errorNel.head}", + user => s"Success: $user", + (warnings, user) => s"Warning: ${user.name.value}; The following warnings occurred: ${warnings.show}" +) + +``` +Similar to [Validated](validated.html), there is also a type alias for using a `NonEmptyList` on the left side. + +```tut:silent +type IorNel[B, A] = Ior[NonEmptyList[B], A] +``` + + +```tut + +val left: IorNel[String, Int] = Ior.leftNel("Error") + +``` + + +We can also convert our `Ior` to `Either`, `Validated` or `Option`. +All of these conversions will discard the left side value if both are available: + +```tut +Ior.both("Warning", 42).toEither +```