There are a number of approaches you can take here, but nullable is certainly a fair option and there are idiomatic ways to deal with null
in Kotlin and Arrow.
Let's take an example, if you're already working with Either
, or typed errors, than I personally prefer to use domain specific errors. Since I try to always follow DDD.
EntityNotFound
is often not more descriptive than null
but UserNotFound
or ArticleNotFound
is much more precise about what went wrong. So if you DDD then I would recommend modelling your errors precisely like that.
sealed interface UserError
data class UserNotFound(val uuid: UUID): UserError
data class UserAlreadyExists(val username: String): UserError
data class User(val uuid: UUID)
interface UserPersistence {
suspend fun insert(user: User): Either<UserAlreadyExists, UUID>
suspend fun fetch(user: UUID): Either<UserNotFound, User>
}
Alternatively you could user User?
instead of Either<UserNotFound, User>
and use ensureNotNull
inside either
to achieve the same behavior.
interface UserPersistence {
suspend fun insert(user: User): Either<UserAlreadyExists, UUID>
suspend fun fetchOrNull(user: UUID): User?
suspend fun fetch(uuid: UUID): Either<UserNotFound, User> =
either { ensureNotNull(fetchOrNull(uuid)) { UserNotFound(uuid) }
}
This shows that the two can easily be converted between each-other. ensureNotNull
also applies smart-casting on the value passed to it, similar to kotlin.requireNotNull
of if(value != null)
so you get all the goodies from the Kotlin language as well.
You can find a full example of a repository layer here, https://github.com/nomisRev/ktor-arrow-example/blob/main/src/main/kotlin/io/github/nomisrev/repo/UserPersistence.kt