Ior

Ior represents an inclusive-or relationship between two data types. This makes it very similar to the Either data type, which represents an “exclusive-or” relationship. An Ior<A, B> (also written as A Ior B) can contain either an A, a B, or both. 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:

fun <D> map(f: (B) -> D): Ior<A, D>

We can create Ior values using Ior.Left, Ior.Right and Ior.Both:

import arrow.*
import arrow.data.*

Ior.Right(42)
// Right(value=42)
Ior.Left("Error")
// Left(value=Error)
Ior.Both("Warning", 41)
// Both(leftValue=Warning, rightValue=41)

Arrow also offers extension functions for Ior, the leftIor, rightIor and bothIor:

3.rightIor()
// Right(value=3)
"Error".leftIor()
// Left(value=Error)
("Warning" to 3).bothIor()
// Both(leftValue=Warning, rightValue=3)

When we look at the Monad or Applicative instances of Ior, we can see that they actually require a Semigroup instance on the left side. This is because Ior will actually accumulate failures on the left side, very similarly to how the Validated 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, 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 are able to do that:

data class User(val name: String, val pw: String)

fun validateUsername(username: String) = when {
    username.isEmpty() -> Nel.of("Can't be empty").leftIor()
    username.contains(".") -> (Nel.of("Dot in name is deprecated") to username).bothIor()
    else -> username.rightIor()
}

fun validatePassword(password: String) = when {
    password.length < 8 -> Nel.of("Password too short").leftIor()
    password.length < 10 -> (Nel.of("Password should be longer") to password).bothIor()
    else -> password.rightIor()
}

fun validateUser(name: String, pass: String) =
        Ior.monad<Nel<String>>().binding {
            val username = validateUsername(name).bind()
            val password = validatePassword(pass).bind()
            User(username, password)
        }.fix()

Now we’re able to validate user data and also accumulate non-fatal warnings:

validateUser("John", "password12")
//Right(value=User(name=John, pw=password12))
validateUser("john.doe", "password")
//Both(leftValue=NonEmptyList(all=[Dot in name is deprecated, Password should be longer]), rightValue=User(name=john.doe, pw=password))
validateUser("jane", "short")
//Left(value=NonEmptyList(all=[Password too short]))

To extract the values, we can use the fold method, which expects a function for each case the Ior can represent:

validateUser("john.doe", "password").fold(
        { "Error: ${it.head}" },
        { "Success $it" },
        { warnings, (name) -> "Warning: $name; The following warnings occurred: ${warnings.show()}" }
)
//Warning: john.doe; The following warnings occurred: Dot in name is deprecated, Password should be longer

Similar to Validated, there is also a type alias for using a NonEmptyList on the left side.

typealias IorNel<A, B> = Ior<Nel<A>, B>
Ior.leftNel<String, Int>("Error")
// Left(value=NonEmptyList(all=[Error]))
Ior.bothNel("Warning", 41)
// Both(leftValue=NonEmptyList(all=[Warning]), rightValue=41)

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:

Ior.Both("Warning", 41).toEither()
// Right(b=41)
Ior.Both("Warning", 41).toValidated()
// Valid(a=41)
Ior.Both("Warning", 41).toOption()
// Some(41)

Available Instances

Credits

Contents partially adapted from Cats Ior