Algebraic Data Types In Kotlin

Algebraic Data Types In Kotlin

Mike Gehard

Lately I have been doing a good amount of reading on functional programming, specifically Haskell and Elm. As part of this reading, I've been exposed to the wonderful world of type systems more advanced than the ones that I am used to, i.e. the Java type system. Exposure to algebraic data types (ADTs) is one of the things that I've enjoyed about these readings. In the rest of this article, I will demonstrate how ADTs can be used in the Kotlin type system to assure that you've handled all of the possible outcomes from a business use case. For those already familiar with the Either type much of this will be old news to you.

Algebraic data types allow me to create a closed set of possible options for a specific type in my domain.

sealed class CreateSubscriptionResult {
    class Success(val subscription: Subscription): CreateSubscriptionResult()
    class Failure(val errors: List<String>): CreateSubscriptionResult()
}

In this case I am using the sealed keyword to tell the type system that there will not be any more possible outcomes for a CreateSubscriptionResult. Now I can use the when keyword to force the consumer of the CreateSubscriptionResult to make sure it handles all of the possible outcomes.

return when (result) {
    is CreateSubscriptionResult.Success ->
        // Do something on success
    is CreateSubscriptionResult.Failure ->
        // Do something on failure
}

Were I to omit one of the possible outcomes,

return when (result) {
    is CreateSubscriptionResult.Success ->
        // Do something on success
}

then the Kotlin compiler will tell me that I've forgotten something.

when expression must be exhaustive, add necessary 'is Failure' branch or 'else' branch instead

Well isn't that nice. I now can use the type system to remind myself, and my fellow developers, that something is missing and keep those types of bugs from cropping up in my software without a lot of boilerplate code. If another outcome is added at some point, the compiler will tell me that and I can then figure out how to handle it.

By using the type system to do this, I enable a faster feedback loop than had I written a test for it. Yes I may still need a test for the logic inside of each branch but I would postulate that if I keep it simple enough (like a difference in response code) that a test may be overkill because of the type system assurance. I'll leave that decision up to you.

One place I have been experimenting with this type of pattern is in my Spring controllers. I like how it makes the code that handles the outcomes easy to read and understand. Another benefit is that I now have an explicit contract between the use case and the consumer that outlines all of the possible outcomes for the use case. When I combine this contract with the when keyword, the compiler will enforce that the client either handles each outcome or decides to explicitly punt on some by using the else keyword.

Have some feedback? I'd love to hear it. Reach out to me on Twitter @mikegehard and we can have a conversation about it.