3 Reasons Enums with superpowers Are Rare in Enterprise Software

You might have heard of "Enums with superpowers", "Tagged Union Types" or Algebraic Data Types(ADT). It is heavily used in functional programming. And yet, you rarely see them in enterprise codebases. Why is that?

A common use case for ADTs is to represent user roles.

{-| A User is either a Guest, Admin or Customer
-}
type User 
  = Guest
  | Admin { name : String }
  | Customer { name : String, address : String }

Kotlin and Java have a similar concept: Sealed interfaces.

sealed interface User
class Guest() : User
class Admin(String name) : User
class Customer(String name, String address) : User

The benefit of using ADTs over regular interfaces is to be able to do exhaustive pattern matching and smart casting.

when(user) {
    is Guest -> 
        "Hi guest"
    is Admin ->
        "Hi" ++ user.name
    is Customer ->
        "Hi" ++ user.name ++ ", your address is " ++ user.address
    //no else branch needed
}

Most modern languages nowadays have implemented similar concepts.

However, in practice, I often see a composition of a base class and an enum instead.

enum class Role { Guest, Admin, Customer }
class User(
  role : Role,
  //Guests don't have a name
  name : String?,
  //Only Customers have an address
  address : String?
)

In a functional language, I would consider this a code smell. I usually want to model my data, such that impossible states can't be represented.

However, at work, this is not only common, I would even call it idiomatic.

So here are my three reasons why I believe that ADTs actually are not practical for professional codebases.

1. Unfamiliar Syntax and Behavior

At least for Kotlin, I never really got used to the syntax, and I heard similar things from my colleges. Whenever someone encounters a sealed interface, they stumble. It's not something they see regularly, and also not too clear just from the syntax alone what it actually does.

I tried to link to good resources about that topic, even added a comment to the class. However, whenever I proposed to use sealed interfaces for a new feature, I had to reintroduce the concept all over again.

I do not have the same problem with other functional concepts.

2. Exhausted Pattern Matching is rarely needed

In production code, you rarely have to match over all cases. Most of the time you just want to access a field or use a default value. A simple null check is enough.

Whenever you do need to distinguish between different roles, you can always use the enum to achieve exhaustiveness and manually cast to the correct type.

3. Missing Support in Data Formats

Data Formats like JSON typically don't support ADTs. The reason being that data does not have to follow rules. If a field is null, it can be discarded.

{ "role" : "admin",
  "name" : "Json"
}

In functional languages there is usually some basic support for encoding/decoding ADTs, however there is no standard way of doing so, and therefore even in functional languages you tend to fall back to using simpler structures for DTOs.

A similar argument applies to SQL-based Databases. By having to use columns, you tend to use composition over ADTs. Having an ADT represent a Database table also has the downside, that you lose the 1:1 relation between fields and columns.

So if you already have a DTO that uses an enum, it's hard to give an argument why you should use an additional ADT. In my eyes this is the killing argument.

Conclusion

In languages where object-oriented programming is not possible, ADTs provide a way to express different shapes of data. They are flexible and the basis for more advanced structures like State Machines, Domain Specific Languages and Effect Systems.

However, in hybrid languages (as most languages nowadays are) you don't need any of these advanced structures to describe side effects. You can mutate state, throw errors and cast at will. While ADTs are a necessity in pure functional languages, they become a commodity in hybrid languages. The actual benefit of ADTs (being exhaustively matchable) is rarely needed. The argument that they are harder to use as DTOs is strong enough, that I do not recommend using them for enterprise code.