EXPEDIA GROUP TECHNOLOGY — ENGINEERING

Functional Programming with Kotlin Arrow

Handle errors cleanly and make temporal dependencies explicit

Photo by Pixabay on Pexels

A lot of Kotlin features can be traced back to Functional Programming languages, such as:

However, Kotlin is missing many incredibly useful features, like Either and Try, which are ubiquitous in functional programming languages.

Kotlin ended up with competing libraries providing such features, but the authors realized it would be better to team up, so the competing libraries have been merged and the Arrow library is now the “standard” functional library for Kotlin.

Why should we care?

Let’s look at a fictitious Java code example:

class PasswordResource {    Response changePassword (
String userId,
String currentPassword,
String desiredPassword
){
changePasswordService
.changePassword(
userId,
currentPassword,
desiredPassword
);
return Response.ok();
}
}
class PasswordService { void changePassword(
String userId,
String currentPassword,
String desiredPassword
){
authenticationThingie.authenticate(userId, currentPassword);

passwordDao
.getMostRecentPasswordHashes(userId)
.stream()
.forEach(recentPasswordHash -> {
if (recentPasswordHash.equalsIgnoreCase(hash(desiredPassword))) {
throw new ReusedPasswordException();
}
});

passwordDao.changePassword(userId, desiredPassword);
}
}
class AuthenticationThingie { void authenticate(
String userId,
String currentPassword,
String desiredPassword
){
throw new NotAuthenticatedException();
}

}
class PasswordDao { void changePassword(
String userId,
String desiredPassword
){
throw new ChangePasswordException();
}

}
class GlobalExceptionHandler { handle(Exception e){
if (e instanceof NotAuthenticatedException) {
return Response.unauthorized();
}
if (e instanceof ChangePasswordException) {
return Response.serverError();
}
}

}

A few things to note:

  • There are implicit temporal dependencies: Before calling passwordDao.changePassword, the userId is authenticated and the password check to be sure it isn’t a reused one, but there’s no compile time enforcement of this.
  • Despite the methods declaring they are void (“I don’t return anything”), the call to passwordService.changePassword does have results — it can be successful or result in three different exceptions thrown by our own code, none of which are listed in the method signatures. In order to find this out, the developer has to read through each and every line of every method potentially called as a result of calling passwordService.changePassword.
  • NotAuthenticatedException and ChangePasswordException are not caught anywhere in the code in context. They are caught in some completely different part of the codebase, maybe because NotAuthenticatedError was considered generic enough to have been wired up as part of work on some other endpoint (which is itself problematic, because you might want to return different responses for the same error in different contexts) and someone saw that NotAuthenticatedException was caught in the GlobalExceptionHandler so they added the ChangePasswordException there too, even though it shouldn’t be there either.
  • ReusedPasswordException isn’t caught by anything, because the compiler doesn’t force anyone to, and will result in an overlooked 500.

With Kotlin Arrow, this could be refactored to

    typealias AuthenticatedUserId = String    typealias NonReusedPassword = String    sealed class ChangePasswordSuccesses {
class ChangePasswordSuccess() : ChangePasswordSuccesses()
}
sealed class ChangePasswordErrors {

class NotAuthenticated() : ChangePasswordErrors()
class ChangePasswordError() : ChangePasswordErrors()
class ReusedPassword() : ChangePasswordErrors()

}
class AuthenticationThingie { fun authenticate(
userId: String,
currentPassword: String
) : Either<NotAuthenticated, AuthenticatedUserId> {
//...authenticate
}

}
class PasswordDao { fun changePassword(
userId: AuthenticatedUserId,
desiredPassword: NonReusedPassword
) : Either<ChangePasswordError, ChangePasswordSuccess> {
//...change the password
}

}
class PasswordService { fun nonReusedPassword(
userId: AuthenticatedUserId,
desiredPassword: String
) : Either<ChangePasswordErrors, Pair<AuthenticatedUser, NonReusedPassword>> {
//...check for reused password
}
fun changePassword(
userId: String,
currentPassword: String,
desiredPassword: String
) : Either<ChangePasswordErrors, ChangePasswordSuccess> {

return authenticationThingie
.authenticate(
userId,
currentPassword
) //Either<NotAuthenticated, AuthenticatedUserId>
.flatMap { authenticatedUser ->
nonReusedPassword(
authenticatedUser,
desiredPassword
) //Either<ReusedPassword, NonReusedPassword>
}.flatMap { userAndPassword ->
passwordDao
.changePassword(
userAndPassword.first,
userAndPassword.second
) //Either<ChangePasswordError, Success>
}
}

}
}class PasswordResource { fun changePassword(
userId: String,
currentPassword: String,
desiredPassword: String
) : Response {

return passwordService
.changePassword(
userId,
currentPassword,
desiredPassword
) //Either<ChangePasswordErrors, ChangePasswordSuccess>
.fold({ //left (error) case
when (it) {
is ChangePasswordErrors.NotAuthenticated -> { Response.status(401).build() }
is ChangePasswordErrors.ChangePasswordError -> { Response.status(500).build() }
is ChangePasswordErrors.ReusedPassword -> { Response.status(400).build() }
}
}, { //right case
return Response.ok()
})

}

}

Note:

  • Implicit temporal dependencies have been made explicit — to call passwordDao.changePassword, a NonReusedPassword is required, and the only way to get one is from the nonReusedPassword method.
  • Methods no longer return things they say they don’t. At every layer, each method explicitly says in the method signature what it returns. There’s no need to look around each and every line of every method in every layer. Developers can tell from a glance at the resource method what the endpoint will return for each outcome, in context.
  • Errors are clearly enumerated in a single place.
  • Errors are guaranteed to be exhaustively mapped because Kotlin enforces that sealed classes are exhaustively mapped at compile time. So a 500 resulting from forgetting to catch a ReusedPasswordException is impossible, and if new errors are added without being mapped to HTTP responses, the compiler will let us know.

This is just one out of countless examples of how data types like Either can be incredibly useful, increase safety by moving more errors to compile time etc etc.

A common criticism of this style is that it’s verbose, there are too many types, and it can be hard to follow if you’re not used to it. The thing is, that’s the price you pay for more accurately modelling the computation at each step, and as we’ve seen, the more imperative alternative is “easy to follow” only because it omits important things that can go wrong at each step, which doesn’t mean they’re not there — they are there, they’re just hidden in different implicit code paths spread across the layers.

I obviously like this style of programming a lot, but this is not just my opinion — hopefully, by now you can see from the above why this style and these types are ubiquitous in functional programming languages. The Expedia Group™️ Partner Central Access team has successfully been using Arrow for about a year or more now, and I’m glad to see it’s slowly being considered for adoption in other teams as well.

I hope I’ve piqued your interest enough to consider using Arrow in your projects.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store