Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What are sealed classes in Kotlin?

Tags:

kotlin

I'm a beginner in Kotlin and recently read about Sealed Classes. But from the doc the only think I actually get is that they are exist.

The doc stated, that they are "representing restricted class hierarchies". Besides that I found a statement that they are enums with superpower. Both aspects are actually not clear.

So can you help me with the following questions:

  • What are sealed classes and what is the idiomatic way of using ones?
  • Does such a concept present in other languages like Python, Groovy or C#?

UPDATE: I carefully checked this blog post and still can't wrap my head around that concept. As stated in the post

Benefit

The feature allows us to define class hierarchies that are restricted in their types, i.e. subclasses. Since all subclasses need to be defined inside the file of the sealed class, there’s no chance of unknown subclasses which the compiler doesn’t know about.

Why the compiler doesn't know about other subclasses defined in other files? Even IDE knows that. Just press Ctrl+Alt+B in IDEA on, for instance, List<> definition and all implementations will be shown even in other source files. If a subclass can be defined in some third-party framework, which not used in the application, why should we care about that?

like image 992
oreh Avatar asked Jun 09 '18 08:06

oreh


1 Answers

Say you have a domain (your pets) where you know there is a definite enumeration (count) of types. For example, you have two and only two pets (which you will model with a class called MyPet). Meowsi is your cat and Fido is your dog.

Compare the following two implementations of that contrived example:

sealed class MyPet class Meowsi : MyPet() class Fido : MyPet() 

Because you have used sealed classes, when you need to perform an action depending on the type of pet, then the possibilities of MyPet are exhausted in two and you can ascertain that the MyPet instance will be exactly one of the two options:

fun feed(myPet: MyPet): String {     return when(myPet) {        is Meowsi -> "Giving cat food to Meowsi!"        is Fido -> "Giving dog biscuit to Fido!"      } } 

If you don't use sealed classes, the possibilities are not exhausted in two and you need to include an else statement:

open class MyPet class Meowsi : MyPet() class Fido : MyPet()  fun feed(myPet: MyPet): String {     return when(myPet) {        is Mewosi -> "Giving cat food to Meowsi!"        is Fido -> "Giving dog biscuit to Fido!"         else -> "Giving food to someone else!" //else statement required or compiler error here     } } 

In other words, without sealed classes there is not exhaustion (complete coverage) of possibility.

Note that you could achieve exhaustion of possiblity with Java enum however these are not fully-fledged classes. For example, enum cannot be subclasses of another class, only implement an interface (thanks EpicPandaForce).

What is the use case for complete exhaustion of possibilities? To give an analogy, imagine you are on a tight budget and your feed is very precious and you want to ensure you don't end up feeding extra pets that are not part of your household.

Without the sealed class, someone else in your home/application could define a new MyPet:

class TweetiePie : MyPet() //a bird 

And this unwanted pet would be fed by your feed method as it is included in the else statement:

       else -> "Giving food to someone else!" //feeds any other subclass of MyPet including TweetiePie! 

Likewise, in your program exhaustion of possibility is desirable because it reduces the number of states your application can be in and reduces the possibility of bugs occurring where you have a possible state where behaviour is poorly defined.

Hence the need for sealed classes.

Mandatory else

Note that you only get the mandatory else statement if when is used as an expression. As per the docs:

If [when] is used as an expression, the value of the satisfied branch becomes the value of the overall expression [... and] the else branch is mandatory, unless the compiler can prove that all possible cases are covered with branch conditions

This means you won't get the benefit of sealed classes for something like this):

fun feed(myPet: MyPet): Unit {     when(myPet) {         is Meowsi -> println("Giving cat food to Meowsi!") // not an expression so we can forget about Fido     } } 

To get exhaustion for this scenario, you would need to turn the statement into an expression with return type.

Some have suggested an extension function like this would help:

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

Then you can do:

fun feed(myPet: MyPet): Unit {     when(myPet) {         is Meowsi -> println("Giving cat food to Meowsi!")      }.exhaustive // compiler error because we forgot about Fido } 

Others have suggested that an extension function pollutes the namespace and other workarounds (like compiler plugins) are required.

See here for more about this problem.

like image 112
David Rawson Avatar answered Sep 21 '22 13:09

David Rawson