This article was originally published at 47deg.com on April 13, 2021.
In a previous blog post about Functional Domain Modeling in Kotlin, we discussed how we can use data class
, enum class
, sealed class
, and inline class
to describe our business domain as accurately as possible to achieve more type-safety, maximize the use of the compiler with our domain, and thus, prevent bugs and reduce unit testing.
At the end, we briefly discussed how we could use Arrow’s Either
type to bring more type-safety into our business logic. In this blog post, we’ll explore how we can use Either
and Validated
to achieve different goals.
Let’s continue with the domain from the previous blog post, which was Event
.
import java.time.LocalDate
inline class EventId(val value: Long)
inline class Organizer(val value: String)
inline class Title(val value: String)
inline class Description(val value: String)
data class Event(
val id: EventId,
val title: Title,
val organizer: Organizer,
val description: Description,
val date: LocalDate
)
To create a proper Event
, we need to validate that the properties meet certain conditions. To do so, we can create smart constructors that return Either
. They’ll return ValidationError
in the Left
side in the case a condition is not met, or an Event
in the Right
side if all conditions were met.
import arrow.core.Either
import arrow.core.Either.Left
import arrow.core.Either.Right
data class ValidationError(val reason: String)
inline class EventId(val value: Long) {
companion object {
fun create(value: Long): Either<ValidationError, EventId> =
if (value > 0) Right(EventId(value))
else Left(ValidationError("EventId needs to be bigger than 0, but found $value."))
}
}
inline class Organizer(val value: String) {
companion object {
/* Same implementation for Title and Description */
fun create(value: String): Either<ValidationError, Organizer> = when {
value.isEmpty() -> Left(ValidationError("Organizer cannot be empty"))
value.isBlank() -> Left(ValidationError("Organizer cannot be blank"))
else -> Right(Organizer(value))
}
}
}
In the example above, we add create
smart constructors on the companion objects of our domain types. For simplicity, we’ve only shown the implementation for Organizer
since we can use an identical implementation for validation for Title
and Description
. This solution is not yet fool-proof since the constructor of the class is still public
. We’ll discuss this, and other solutions, further in the next blog post about type refinement.
We can now easily construct an Event
using the either
computation block, which allows us to extract the success value to the left-hand side. When encountering a Left
case, it immediately returns with the Left
result. This behavior is commonly referred to as short circuit behavior, since the either
computation block short circuits when encountering a Left
case.
These computation
blocks have support for suspend
. But, in case you don’t want to call it from a suspend fun
, you can use either.eager
instead, which is an implementation using @RestrictSuspensioncode>@RestrictSuspension</code.
import arrow.core.computations.either
suspend fun generateId(): Long =
-1L
suspend fun createEvent(): Either<ValidationError, Event> =
either {
val id = EventId.create(generateId()).bind()
val title = Title.create(" ").bind()
val organizer = Organizer.create("").bind()
val description = Description.create("").bind()
Event(id, title, organizer, description, LocalDate.now())
} // Left(ValidationError("EventId needs to be bigger than 0, but found -1."))
Since the validation of EventId
failed, the either
block immediately returns with Left
for EventId
. So, we have no idea that Title
, Organizer
, and Description
were also incorrect because Either
short circuits with the first encountered Left
.
Imagine using this technique for validating a form to create an Event
, and instead of indicating all incorrect filled fields, it tells you that title is incorrect. And when you fix it, it tells you that organizer name is incorrect, and a third time it’ll tell you that the description is not filled out correctly. That wouldn’t be user-friendly at all.
What we’d like to do instead for validation is to accumulate all errors that we’ve encountered, OR return the success value.
So let’s refactor our above defined validation snippet to Validated
.
import arrow.core.Valid
import arrow.core.ValidatedNel
import arrow.core.invalidNel
data class ValidationError(val reason: String)
inline class EventId(val value: Long) {
companion object {
fun create(value: Long): ValidatedNel<ValidationError, EventId> =
if (value > 0) Valid(EventId(value))
else ValidationError("EventId needs to be bigger than 0, but found $value.").invalidNel()
}
}
inline class Organizer(val value: String) {
companion object {
fun create(value: String): ValidatedNel<ValidationError, Organizer> = when {
value.isEmpty() -> ValidationError("Organizer cannot be empty").invalidNel()
value.isBlank() -> ValidationError("Organizer cannot be blank").invalidNel()
else -> Valid(Organizer(value))
}
}
}
As you can see, very little changed between our Either
based code and our Validated
based code. That’s because they model the same kind of relationship, which we’ve discussed in our previous blog post. The OR relationship and both have two cases: Left
/Invalid
and Right
/Valid
.
As you might’ve guessed, the difference between the two is that Either
short circuits on Left
, and Validated
accumulates the errors in Invalid
.
So if we try to construct an Event
using the Validated
type instead of Either
, we’ll be able to figure out all errors that occurred trying to construct the Event
.
To do so, we’re using ValidatedNel
, which is typealias ValidatedNel<E, A> = Validated<NonEmptyList<E>, A>
. This allows us to accumulate all errors into NonEmptyList
.
Using NonEmptyList
instead of List
is more precisely modeled since there will always be at least one ValidationError
; otherwise we’d expect a Valid
Event
.
suspend fun generateId(): Long =
-1L
suspend fun date(): LocalDate =
LocalDate.now()
suspend fun createEvent(): ValidatedNel<ValidationError, Event> =
EventId.create(generateId()).zip(
Title.create(""),
Organizer.create(""),
Description.create("")
) { id, title, organizer, description -> Event(id, title, organizer, description, date()) }
//Invalid(NonEmptyList(
// ValidationError("EventId needs to be bigger than 0, but found -1."),
// ValidationError("Title cannot be blank"),
// ValidationError("Organizer cannot be empty"),
// ValidationError("Description cannot be empty")
//))
In the resulting value, we find all accumulated errors that occurred while trying to construct an Event
.
Delegating our call to zip
allows us to combine independent values. In order to combine these values, we also need to provide a way to combine the Invalid
cases. Here we use Semigroup.nonEmptyList()
by default for ValidatedNel
. Semigroup
is a functional interface that defines how to combine
associatively. In the case of NonEmptyList
, it simply delegates to NonEmptyList#plus
, which results in a non-empty list of all elements.
We can use suspend
functions both in the zip
parameters, and in the map
lambda since the zip
function is inline
in its definition. This constructor is also available for Either
, while maintaining the short circuit behavior of Either
and thus not requiring a Semigroup
.
At this point, we may be wondering when to use Either
or when to use Validated
. Generally speaking, we would choose Either
when we want a short-circuiting behavior while validating values; that is for the computation to stop whenever we encounter an invalid case. We would use Validated
, especially its ValidatedNel
form, when we want to accumulate invalid cases over multiple independent validations and therefore not short-circuiting on failure.
In all use cases, toEither
or toValidated
serve as conversion functions. Furthermore, both Validated
and Either
values are accepted inside either
computation blocks and they’re able to .bind()
and short-circuit via suspension when needed.
suspend fun createEvent(): Validated<NonEmptyList<ValidationError>, Event> = ...
suspend fun createEventAndLog(): Either<NonEmptyList<ValidationError>, Unit> =
either {
val event = createEvent().bind()
println(event)
}
Now that we’ve seen how we can use either
computation blocks and zip
, let’s see some other interesting operators and parallel operators.
Often, we have to work with many values, for example a List
of identifiers, which we need to process or validate. A common use case with List
would be to use map
, but this has some undesired results if we use it in combination with another wrapper like Either
or Validated
.
data class User(val id: Long, val name: String)
fun createUser(id: Long, name: String): Either<ValidationError, User> {
val id = if(id > 0) Right(id) else Left(ValidationError("Id of name: $name needs to be bigger than 0, but found $id."))
val name = if (name.isNotBlank()) Right(name) else Left(ValidationError("Name of id: $id cannot be blank"))
return id.zip(name, ::User)
}
fun Iterable<Pair<Long, String>>.createUsers(): List<Either<ValidationError, User>> =
map { (id, name) -> createUser(id, name) }
The result of createUsers
is of the type List<Either<ValidationError, User>>
, which is not very useful because, to do anything with the User
, we have to check every value in the list and check if it’s Left
or Right
, which is cumbersome to do. This also doesn’t take into account the short-circuiting behavior of Either
, and instead runs createUser
for all values also if previous operations resulted in Left
.
So let’s use traverseEither
instead.
import arrow.core.traverseEither
fun Iterable<Pair<Long, String>>.createUsers(): Either<ValidationError, List<User>> =
ids.traverseEither { (id, name) -> createUser(id, name) }
traverseEither
applies a function to every value inside the collection and returns an Either
with the List
of all successful applications of the function.
On the other hand, traverseEither
will short circuit if it encounters a Left
value and it immediately returns it and ignores the rest of elements.
We can do the same thing for Validated
using traverseValidated
and supplying a Semigroup
. When doing so with Validated
instead of short circuit, it will accumulate all the errors of all the values in the collection.
import arrow.core.traverseValidated
fun createUser(id: Long, name: String): ValidatedNel<ValidationError, User> {
val id = if(id > 0) Valid(id) else ValidationError("Id of name: $name needs to be bigger than 0, but found $id.").invalidNel()
val name = if (name.isNotBlank()) Valid(name) else ValidationError("Name of id: $id cannot be blank").invalidNel()
return id.zip(name, ::User)
}
suspend fun Iterable<Pair<Long, String>>.createUsers(): ValidatedNel<ValidationError, List<User>> =
ids.traverseValidated { (id, name) -> createUser(id, name) }
As a final example, we’re going to introduce some concurrency with another Arrow library called Arrow Fx Coroutines. In Arrow Fx Coroutines, we can find a traverseXXX
variant that runs the supplied function in parallel, called parTraverseXXX
.
So let’s see the same example for Either
, but let’s introduce parallelism.
import arrow.fx.coroutines.parTraverseEither
fun createUser(id: Long, name: String): Either<ValidationError, User> {
val id = if(id > 0) Right(id) else Left(ValidationError("Id of name: $name needs to be bigger than 0, but found $id."))
val name = if (name.isNotBlank()) Right(name) else Left(ValidationError("Name of id: $id cannot be blank"))
return id.zip(name, ::User)
}
suspend fun Iterable<Pair<Long, String>>.createUsers(): Either<ValidationError, List<User>> =
ids.parTraverseEither { (id, name) -> createUser(id, name) }
Here we’ll launch N
coroutines in parallel, where N
is the size of the List
, where we run the createUser
functions. The short circuit behavior of Either
is maintained, and all still-running parallel tasks will be cancelled before the function returns the first encountered Left
case.
When applying the same technique to Validated
, it will instead run all parallel tasks to finish, and it will accumulate all the parallel encountered errors.
import arrow.core.typeclasses.Semigroup
import arrow.fx.coroutines.parTraverseValidated
fun Iterable<Pair<Long, String>>.createUsers(): ValidatedNel<ValidationError, List<User>> =
ids.parTraverseValidated(Semigroup.nonEmptyList()) { (id, name) -> createUser(id, name) }
This makes parTraverseValidated
a powerful function to do concurrent validation without any boilerplate by just using the Validated
data type. And parTraverseEither
is a powerful function for combining operations in parallel when we’re only interested in the result if all tasks succeed.
Either
, Validated
, and traverse
can be found inside Arrow Core
, and the parallel operators can be found in Arrow Fx Coroutines.
depdendencies {
def arrowVersion = "0.13.1"
implementation "io.arrow-kt:arrow-core:$arrowVersion"
implementation "io.arrow-kt:arrow-fx-coroutines:$arrowVersion"
}
In this post, we’ve seen how we can improve our domain by:
- Using
Either
when we’re only interested in the first error OR the success value. - Using
Validated
when we’re interested in all errors OR the success value. - Using
traverse
tomap
anIterable
that returnsEither
orValidated
- Using
parTraverse
to parallel map anIterable
that returnsEither
orValidated
.
Some of you might have noticed that issues remain with some of the code above, which we’ll discuss in the next and final blog post of functional domain modeling about type refinement in [Kotlin](https://xebia.com/blog/7-common-mistakes-in-kotlin/ “Kotlin”)! So stay tuned for more functional domain modeling in Kotlin.