From 2e175b4dc8ab85ae42708e01e21f138df060e7fb Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Thu, 17 Aug 2017 19:51:10 +0200 Subject: [PATCH 1/6] Add Ior docs --- docs/src/main/tut/datatypes/ior.md | 114 +++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 docs/src/main/tut/datatypes/ior.md diff --git a/docs/src/main/tut/datatypes/ior.md b/docs/src/main/tut/datatypes/ior.md new file mode 100644 index 0000000000..69dd4fc60a --- /dev/null +++ b/docs/src/main/tut/datatypes/ior.md @@ -0,0 +1,114 @@ +--- +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 `Xor` 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.Ior + +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.of("Can't be empty").leftIor + else if (u.contains(".")) + Ior.both(Nel.of("Dot in name is deprecated"), Username(u)) + else + Username(u).rightIor +} + +def validatePassword(p: String): Failures Ior Password = { + if (p.length < 8) + Nel.of("Password too short").leftIor + else if (p.length < 10) + Ior.both(Nel.of("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}" +) + +``` + + +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 +``` From c64bb16e5982607c20a1cc2cc857f0ced325dbd4 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Fri, 25 Aug 2017 21:57:06 +0200 Subject: [PATCH 2/6] Add menu entry --- docs/src/main/resources/microsite/data/menu.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/main/resources/microsite/data/menu.yml b/docs/src/main/resources/microsite/data/menu.yml index 081dfffe2a..038c0ae5db 100644 --- a/docs/src/main/resources/microsite/data/menu.yml +++ b/docs/src/main/resources/microsite/data/menu.yml @@ -132,6 +132,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 From 3435ef5b634e0ffd23708bd07d7eaec6aadf9ca4 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Tue, 12 Sep 2017 17:58:42 +0200 Subject: [PATCH 3/6] Replace Xor reference --- docs/src/main/tut/datatypes/ior.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/tut/datatypes/ior.md b/docs/src/main/tut/datatypes/ior.md index 69dd4fc60a..f9f8cf9633 100644 --- a/docs/src/main/tut/datatypes/ior.md +++ b/docs/src/main/tut/datatypes/ior.md @@ -8,7 +8,7 @@ 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 `Xor` relationship. +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. From 26307f9fb70f0fb1378e7ac9c59a4762e0e2999e Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Tue, 12 Sep 2017 23:19:39 +0200 Subject: [PATCH 4/6] Switch to Nel#one --- docs/src/main/tut/datatypes/ior.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/main/tut/datatypes/ior.md b/docs/src/main/tut/datatypes/ior.md index f9f8cf9633..6460968d99 100644 --- a/docs/src/main/tut/datatypes/ior.md +++ b/docs/src/main/tut/datatypes/ior.md @@ -60,18 +60,18 @@ case class User(name: Username, pw: Password) def validateUsername(u: String): Failures Ior Username = { if (u.isEmpty) - Nel.of("Can't be empty").leftIor + Nel.one("Can't be empty").leftIor else if (u.contains(".")) - Ior.both(Nel.of("Dot in name is deprecated"), Username(u)) + 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.of("Password too short").leftIor + Nel.one("Password too short").leftIor else if (p.length < 10) - Ior.both(Nel.of("Password should be longer"), Password(p)) + Ior.both(Nel.one("Password should be longer"), Password(p)) else Password(p).rightIor } From fab1ea3564cdce14a0f60b5604adeabe68919834 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Thu, 14 Sep 2017 16:16:56 +0200 Subject: [PATCH 5/6] Ior.leftNel --- docs/src/main/tut/datatypes/ior.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/main/tut/datatypes/ior.md b/docs/src/main/tut/datatypes/ior.md index 6460968d99..57a134934f 100644 --- a/docs/src/main/tut/datatypes/ior.md +++ b/docs/src/main/tut/datatypes/ior.md @@ -60,7 +60,7 @@ case class User(name: Username, pw: Password) def validateUsername(u: String): Failures Ior Username = { if (u.isEmpty) - Nel.one("Can't be empty").leftIor + Ior.leftNel("Can't be empty") else if (u.contains(".")) Ior.both(Nel.one("Dot in name is deprecated"), Username(u)) else @@ -69,7 +69,7 @@ def validateUsername(u: String): Failures Ior Username = { def validatePassword(p: String): Failures Ior Password = { if (p.length < 8) - Nel.one("Password too short").leftIor + Ior.leftNel("Password too short") else if (p.length < 10) Ior.both(Nel.one("Password should be longer"), Password(p)) else From 01aa2f08fc69c7c5a5573d8ae70c2d39b3e20511 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Fri, 15 Sep 2017 16:56:38 +0200 Subject: [PATCH 6/6] Add note to IorNel --- docs/src/main/tut/datatypes/ior.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/src/main/tut/datatypes/ior.md b/docs/src/main/tut/datatypes/ior.md index 57a134934f..82fb808bac 100644 --- a/docs/src/main/tut/datatypes/ior.md +++ b/docs/src/main/tut/datatypes/ior.md @@ -21,7 +21,7 @@ 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.Ior +import cats.data._ val right = Ior.right[String, Int](3) @@ -60,7 +60,7 @@ case class User(name: Username, pw: Password) def validateUsername(u: String): Failures Ior Username = { if (u.isEmpty) - Ior.leftNel("Can't be empty") + Nel.one("Can't be empty").leftIor else if (u.contains(".")) Ior.both(Nel.one("Dot in name is deprecated"), Username(u)) else @@ -69,7 +69,7 @@ def validateUsername(u: String): Failures Ior Username = { def validatePassword(p: String): Failures Ior Password = { if (p.length < 8) - Ior.leftNel("Password too short") + Nel.one("Password too short").leftIor else if (p.length < 10) Ior.both(Nel.one("Password should be longer"), Password(p)) else @@ -103,6 +103,18 @@ validateUser("john.doe", "password").fold( (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") + ```