Sum Types in Swift and Kotlin

Photo by Roman Mager on Unsplash

Before jumping straight into a comparison, it is handy to know what a Sum Type is. Let’s go a little meta!

When programming, one often needs to create a Composite Type. That is, a type combining other types. In functional programming, it’s often referred to as an Algebraic Data Type (not to be confused with Abstract Data Type). There are two common types of Algebraic Data Types, one being the Product Type (think classes/structs/records, tuples) and the other being the Sum Type (think discriminated/tagged unions and variants).

Let’s take a look at the definition of a Sum Type from Wikipedia (slightly cropped, but no words have been changed):

In computer science, a sum type, is a data structure used to hold a value that could take on several different, but fixed, types.

In practical terms, a Sum Type can be thought of as an enum, with a payload (where that payload is data). So practically, what does this mean? Again from Wikipedia:

Only one of the types can be in use at any one time, and a tag field explicitly indicates which one is in use. It can be thought of as a type that has several “cases,” each of which should be handled correctly when that type is manipulated.

Why is it called a sum type? You could think of it as being an ‘or’ type, where the number of states it could be in is defined by summing them all together. Think of a boolean, it’s a classic sum type. A boolean’s possible set of values is limited to true or false. An integer, depending on what type of integer, will be its maximum size (assuming it is unsigned). With a Product Type, the number of possible states it can be in is calculated using the Cartesian product. The Cartesian product is the “set of all (x, y, z, ..)” where x, y, z are types. So if you have a struct with two booleans, each boolean’s range is two (true or false), and so you calculate its range as 2 multiplied by 2. Just to hammer in, another example would be a struct with a boolean and two integers. Let’s assume these are 8 bit integers, and so have a range of 255. This struct’s product would be 2 (for the boolean) multiplied by 255 (for the first integer) multiplied by 255 (for the second integer). Generally, a Sum Type will have a smaller footprint in your ability to reason about what state it’s in compared to a Product Type.

Getting clearer? Given the following use case, let’s take a practical look at how this can be achieved in Swift and Kotlin.

Example Use Case: Event Logger

Your app requires event logging. Different events require different data. In order to ensure the compiler knows what data is required for each event, the logging will be done internally to the Sum Type. This way we can avoid forgetting to log specific pieces of information in our client code. As each case requires an object to be constructed, if you’re missing data, the code won’t compile.


Swift Example

enum EventLogger {
    case login(userType: UserType)
    case logout(userType: UserType)
    case viewMedia(fileName: String, userType: UserType, mediaType: MediaType)
    case searchMedia(searchTerm: String, userType: UserType, mediaType: MediaType, category: CategoryType)
    
    func logEvent() {
        switch self {
        case let .login(userType):
            print("A \(userType.rawValue) logged in!")
        case let .logout(userType):
            print("A \(userType.rawValue) logged out!")
        case let .viewMedia(fileName, userType, mediaType):
            print("A \(userType.rawValue) viewed \(fileName) which is a \(mediaType.rawValue)")
        case let .searchMedia(searchTerm, userType, mediaType, category):
            print("A \(userType.rawValue) searched for \"\(searchTerm)\" of media type \(mediaType.rawValue) in the \(category.rawValue)")
        }
    }
}
view raweventlogger.swift hosted with ❤ by GitHub

In Swift, Sum Types are supported through the enum keyword. Sum types are built into the language. Looking at each case, you can see that it’s a case type and it has some data.

// The case type (viewMedia), and the payload (String, UserType, MediaType)case viewMedia(fileName: String, userType: UserType, mediaType: MediaType)

The method logEventpattern matches (using a switch case) over what kind of data it could be (login|logout|viewMedia|searchMedia). Note that the default case is not required, we know all possible cases and Swift’s switch statement is exhaustive (i.e. requires all possible paths are covered). It then builds an event message and logs it (okay, prints it to the console).

It’s worth noting that the payload is private to each case, so the only way to get the data out is to unwrap it through pattern matching.

Here’s what the code looks like to construct and log each case

EventLogger
    .login(userType: .admin)
    .logEvent()

EventLogger
    .viewMedia(
        fileName: "cats looking at things",
        userType: .staff,
        mediaType: .video)
    .logEvent()

EventLogger
    .searchMedia(
        searchTerm: "cute dog",
        userType: .customer,
        mediaType: .photo,
        category: .seriousBusiness)
    .logEvent()

EventLogger
    .logout(userType: .customer)
    .logEvent()
view raweventLoggerExample.swift hosted with ❤ by GitHub

Kotlin Example

val <T> T.exhaustivePatternMatching: T
    get() = this

sealed class EventLogger {
    data class Login(
            val userType: UserType
    ) : EventLogger()

    data class Logout(
            val userType: UserType
    ) : EventLogger()

    data class ViewMedia(
            val fileName: String,
            val userType: UserType,
            val mediaType: MediaType
    ) :EventLogger()

    data class SearchMedia(
            val searchTerm: String,
            val userType: UserType,
            val mediaType: MediaType,
            val categoryType: CategoryType
    ) :EventLogger()

    fun logEvent() {
        when (this) {
            is Login ->
                println("A ${userType.typeName} logged in!")
            is Logout ->
                println("A ${userType.typeName} logged out!")
            is ViewMedia ->
                println("A ${userType.typeName} viewed ${fileName} which is a ${mediaType.typeName}")
            is SearchMedia ->
                println("A ${userType.typeName} searched for \"${searchTerm}\" of media type ${mediaType.typeName} in the ${categoryType.typeName}")
        }.exhaustivePatternMatching
    }
}
view raweventlogger.kt hosted with ❤ by GitHub

In Kotlin, Sum Types are supported through inheritance, using sealed classesand data classes which inherit from said sealed class. It looks like a bit of a hack, and perhaps it is. It seems like it wasn’t built into the language and it was more of a discovered feature than a planned feature.

Unfortunately, we can’t just privatise the payload in each case, as pattern matching in Kotlin works a little differently and although it will unwrap our payload in a nice, safe way, we can still just access the members manually. This may not be an issue most of the time, but there will likely be times when it would be nice to hide this.

It should also be noted, that exhaustive pattern matching is not the default functionality in Kotlin unless it’s an expression (i.e. it’s assigning the result of the when block to a variable, or returning the result). To enforce exhaustive pattern matching when not using it as an expression, itmust finish with an empty let block. It isn’t very obvious that that’s what this is doing, so an extension property appropriately named exhaustivePatternMatching was added. The more functional way to do this would be to use it as an expression and return the result rather than use the when block to perform side effects (in this case, the println).

Here’s what the code looks like to construct and log each case.

EventLogger
    .Login(UserType.Admin)
    .logEvent()

EventLogger
    .ViewMedia(
        "cats looking at things",
        UserType.Staff,
        MediaType.Video
    )
    .logEvent()

EventLogger
    .SearchMedia(
        "cute dog",
        UserType.Customer,
        MediaType.Photo,
        CategoryType.SeriousBusiness
    )
    .logEvent()

EventLogger
    .Logout(UserType.Customer)
    .logEvent()
view raweventLoggerExampleKotlin.kt hosted with ❤ by GitHub

The Conclusion

Comparing the two above examples, it could be said that Sum Types are first class citizens in Swift. There isn’t really any hackery involved in getting them to work. In Kotlin, the type system has been “played”. There are a number of things one must remember in order to get the pattern matching to work. Don’t forget to inherit from the sealed class, don’t forget to force exhaustive pattern matching (if using the when block for side effects) and lastly, don’t forget that the member variable should not be accessed outside of the class (but the compiler won’t disallow it).

Previous
Previous

Small Teams and Process

Next
Next

Mind the Gap