· 5 min read
Sealed classes in Kotlin: enums with super-powers (KAD 28)
Sealed classes in Kotlin are another new concept we didn’t have in Java, and open another new world of possibilities.
A sealed class allows you to represent constrained hierarchies in which an object can only be of one of the given types.
That is, we have a class with a specific number of subclasses. What we get in the end is a concept very similar to an enum. The difference is that in the enum we only have one object per type, while in the sealed classes we can have varios objetos de la misma clase.
This difference will allow objects from a sealed class to keep state. This will bring us some advantages that we’ll see in a moment, and also opens the doors to some functional ideas.
How to use sealed classes
Implementing a sealed class is actually very simple. Let’s use as an example a set of operations that can be applied to integers.
The implementation would be as follows:
sealed class Operation {
class Add(val value: Int) : Operation()
class Substract(val value: Int) : Operation()
class Multiply(val value: Int) : Operation()
class Divide(val value: Int) : Operation()
}
We create a sealed class called Operation
, which contains four types of operations: addition, subtraction, multiplication and division.
The good thing about this is that now when
expressions will require us to provide branches for all possible types:
fun execute(x: Int, op: Operation) = when (op) {
is Operation.Add -> x + op.value
is Operation.Substract -> x - op.value
is Operation.Multiply -> x * op.value
is Operation.Divide -> x / op.value
}
If you leave any of the subclasses out, when
will complain and it won’t compile. If you implement them all, you don’t need else
statement. And in general it won’t be recommended because that way we’re sure that we’re doing the right thing for all of them.
This is also great in case you decide to add a new operation, because it’ll fail at compile time and won’t run. Add a couple more operations: increment and decrement:
sealed class Operation {
...
object Increment : Operation()
object Decrement : Operation()
}
You’ll see that the compiler now warns you that there is a problem. Just add branches for these new operations:
fun execute(x: Int, op: Operation) = when (op) {
...
Operation.Increment -> x + 1
Operation.Decrement -> x - 1
}
You may have noticed I did something diferente. I used objects instead of classes. This is because if a subclass doesn’t keep state, it can just be an object. All the instances you create for that class would be exactly the same, as they can’t have different state.
Then, in the when
expression you can get rid of is
for those cases. Here you can just compare the object, as there’s only one instance, you don’t need to check the type of object. It would work too if you keep is
for those too.
If you think about it carefully, a sealed class where all subclasses are objects would be the same as an enum.
Moving side effects to a single point
Side effects are a very recurring concept in functional programming. Functional programming relies heavily on the idea that for a given function, same parameters will return the same result.
Any state that is modified may break this assumption. But any program needs to modify states, communicate with input/output elements, etc. So it’s important to spot these operations very specific places in our code that can be easily isolated.
For example, any operations performed on an Android view can be considered a side effect, as the status of the views is being modified and the functions aren’t aware of it.
We could create a sealed class that would allow us to do operations on our views. Based on the idea of our previous example:
sealed class UiOp {
object Show: UiOp()
object Hide: UiOp()
class TranslateX(val px: Float): UiOp()
class TranslateY(val px: Float): UiOp()
}
fun execute(view: View, op: UiOp) = when (op) {
UiOp.Show -> view.visibility = View.VISIBLE
UiOp.Hide -> view.visibility = View.GONE
is UiOp.TranslateX -> view.translationX = op.px
is UiOp.TranslateY -> view.translationY = op.px
}
Remember: operations that have no state can be objects, because we don’t need different instances.
Now you can create a Ui
object that accumulates all interface operations that we want to do over a view, but it won’t execute them until the moment we want.
We’ll have a description of what we want to do, and then we can create a component that executes them:
class Ui(val uiOps: List<UiOp> = emptyList()) {
operator fun plus(uiOp: UiOp) = Ui(uiOps + uiOp)
}
The Ui
class stores a list of operations, and specifies a sum operator that will help make everything a bit cleaner and easier to read. Now we can specify the list of operations that we want to perform:
val ui = Ui() +
UiOp.Show +
UiOp.TranslateX(20f) +
UiOp.TranslateY(40f) +
UiOp.Hide
run(view, ui)
And then run it. Here I’m just using a run
function, but this could be a complete class if required.
fun run(view: View, ui: Ui) {
ui.uiOps.forEach { execute(view, it) }
}
Imagine the power that gives you this. Right now all you do is run the operations sequentially, but this could be as complex as required.
This run
function could be passed to another function or a class, and the way those operations are run would be totally interchangeable. Remember you can pass functions as arguments.
Conclusion
The concept of the sealed classes is very simple, but it’s the basis of a lot of new ideas you need to get used if you haven’t played with functional programming before.
I must say that I’m not yet able to take the most out of sealed classed due to my knowledge limitations in functional programming.
If all this passionate you as to me, I encourage you to sign up for my free training where I will tell you everything you need to learn about how to create your Android Apps in Kotlin from scratch.