Классы возможных ошибок

Either<E, A>

Ior<E, A>

Raise<E>

Работа с ошибками

object UserNotFound
data class User(val id: Long)

val user: Either<UserNotFound, User> = User(1).right()

fun Raise<UserNotFound>.user(): User = User(1)

val error: Either<UserNotFound, User> = UserNotFound.left()

fun Raise<UserNotFound>.error(): User = raise(UserNotFound)

Перевод either и raise

val res = either { user() }

fun Raise<UserNotFound>.res(): User = user.bind()

Проверки

data class UserNotFound(val message: String)

fun User.isValid(): Either<UserNotFound, Unit> = either {
  ensure(id > 0) { 
    UserNotFound("User without a valid id: $id") }
}

fun Raise<UserNotFound>.isValid(user: User): User {
  ensure(user.id > 0) { 
    UserNotFound("User without a valid id: ${user.id}") }
  return user
}

Анализ ошибок

fun example() {
  when (res) {
    is Left -> fail("No logical failure occurred!")
    is Right -> res.value shouldBe User(1)
  }

  fold(
    block = { res() },
    recover = { _: UserNotFound -> 
        fail("No logical failure occurred!") },
    transform = { i: User -> i shouldBe User(1) }
  )
}

Взаимодействие с исключениями

data class UserAlreadyExists(
    val username: String, val email: String)

suspend fun Raise<UserAlreadyExists>.insertUser(
    username: String, email: String): Long =
  catch({
    UsersQueries.insert(username, email)
  }) { e: SQLException ->
    if (e.isUniqueViolation()) 
        raise(UserAlreadyExists(username, email))
    else throw e
  }

Взаимодействие с исключениями

suspend fun insertUser(
    username: String, email: String
): Either<UserAlreadyExists, Long> =
  Either.catchOrThrow<SQLException, Long> {
    UsersQueries.insert(username, email)
  }.mapLeft { e ->
    if (e.isUniqueViolation()) UserAlreadyExists(username, email)
    else throw e
  }

Работа с несколькими ошибками

data class NotEven(val i: Int)

fun Raise<NotEven>.isEven(i: Int): Int =
  i.also { ensure(i % 2 == 0) { NotEven(i) } }

fun isEven2(i: Int): Either<NotEven, Int> =
  either { isEven(i) }

Работа с несколькими ошибками

val errors = nonEmptyListOf(
    NotEven(1), NotEven(3), NotEven(5), 
    NotEven(7), NotEven(9)).left()

fun example() {
  (1..10).mapOrAccumulate { isEven(it) } shouldBe errors
  (1..10).mapOrAccumulate { isEven2(it).bind() } shouldBe errors
}

Работа с различными ошибками

data class User(val name: String, val age: Int)

sealed interface UserProblem {
  object EmptyName: UserProblem
  data class NegativeAge(val age: Int): UserProblem
}

Работа с различными ошибками. До первой ошибки

data class User private constructor(
    val name: String, val age: Int) {
    companion object {
        operator fun invoke(name: String, age: Int)
            : Either<UserProblem, User> = either {
        ensure(name.isNotEmpty()) { 
            UserProblem.EmptyName }
        ensure(age >= 0) { 
            UserProblem.NegativeAge(age) }
        User(name, age)
    }
  }
}

Работа с различными ошибками. Полная проверка

data class User private constructor(
    val name: String, val age: Int) {
    companion object {
        operator fun invoke(name: String, age: Int): 
            Either<NonEmptyList<UserProblem>, User> = either {
        zipOrAccumulate(
            { ensure(name.isNotEmpty()) { 
                UserProblem.EmptyName } },
            { ensure(age >= 0) { 
                UserProblem.NegativeAge(age) } }
      ) { _, _ -> User(name, age) }
    }
  }
}