Sealed classes design

I found something distressing after update to M14. At first I completely agreed with data class limitations mentioned in one of your blog posts. I thing it's a good idea to prohibit inheritance with data classes but I think you should also dissallow using vars in them. But that's not my point right now.

There is currently one use case for which I think it’s absolutely reasonable and favorable to allow data class to inherit from other class. I really hope this was just overlooked because sealed classes are not used that much yet. But I am using them since the first day they appeared at master branch because I think it’s one of the most powerfull features that was added to the language. But now is my code full of warnings about data class inheritance.

Example:

 
sealed class Size {
  object Small: Size()
  class Big(val x : Int): Size()
}

fun main(args: Array<String>) {
  val sizes = setOf<Size>(Size.Small, Size.Small)   
  println(sizes.size())  // prints 1 – OK

  val sizes2 = setOf<Size>(Size.Big(1), Size.Big(1))
  println(sizes2.size()) // prints 2 – Not OK :frowning:
}


Code now doesn’t work as expected because Size.Big can’t have “data” modifier and doesn’t have correctly defined equals/hashCode. I can define them by hand and enjoy this java-like experience or just return where I was before M13 and use interface instead.

 
interface Size {
    object Small: Size
    data class Big(val x : Int): Size
}

But now I lost exhaustive checking and I'm sad.

Maybe I’m just missing something but sealed classes seems broken to me in current situation.
Two possible solutions came to my mind. Relax your restrictions of data clasess and allow them to inherit from sealed class if it doesn’t have any properties or all properties are abstract. Or allow new syntax: “sealed interface { … }”.

I have second question just because I’m curious. Is your current design of “algebraic sum type” something you deliberately wanted or this is result of some compromises? How to represent this in bytecode, interoperability with Java, or something else? Because since the beginning I was little worried about explicit usage of inheritance and now it culminated with this problem with data classes. Why didn’t you just gave more power to enum and allow its values to have arguments? I mean something like enum in Swift. Because there is sometimes strange overlap between these 2 things:

 
enum class Enum {
    A, B
}

sealed class Sealed {
    object A: Sealed()
    object B: Sealed()
}

Both enum and sealed classes can now represent "the same" concept. Why not merge these 2 things into only one language feature? Sealed class is also much more verbose and contains explicit inheritance (in enum it's hidden behind the curtain). I noticed that because of this it's also harder to explain to java developers what sealed class actually represents (algebraic type) because all they see is just inheritance of classes.

Another thing that I don’t like that much is how type inference works.

 
val x = Enum.A    // inferred type Enum
val y = Sealed.A  // inferred type Sealed.A

I find the enum inferrence much more intuitive and convenient.

2 Likes

>There is currently one use case for which I think it's absolutely  reasonable and favorable to allow data class to inherit from other  class.

Yes, I have cases where I use sealed classes the same way.

+1

Why not to use enum? Like rust do, https://doc.rust-lang.org/book/enums.html

AFAIK the `enum`s in Kotlin are not as flexible as the `enum`s in Rust (BTW a very well designed language). Seems that the design of Java `enum`s and the JVM bytecode imposes some restrictions here. Before `sealed` was introduced I tried to implement ADTs with `enum` and quickly saw that this doesn't work. There are examples for ADTs on GitHub and try.kotlinlang using `interface` as base or `open class` as base, but `sealed` has the advantage that `when()` pattern matching is exhaustive for them.

enums and traits play an integral role in Rust and from what I saw allow very elegant and clean solutions. Now, with Kotlin we have the option to use  sealed class instead of Rust’s enums and I think extension functions and/or class delegation to do something similar to Rust’s traits. But I haven’t experimented deeper, with how to design this in Kotlin.

I'd like to reiterate that the current restrictions on data classes are temporary and will be relaxed after the 1.0 release, once we have time to properly think through and implement a more general design. Your usecase is a good one, and we will definitely attempt to support it after 1.0.

At the risk of polluting the discussion, just wanted to chime in to say support for data classes inheriting from a sealed class would be really useful to us as well. Is there is an issue we can follow somewhere to track progress of extending data classes?

The issue is https://youtrack.jetbrains.com/issue/KT-10330

1 Like

Is there a way to force Kotlin to check all possibilities of a sealed class when i use the when expression ?

Kotlin does exhaustiveness checks only if when is used as an expression (e.g. val x = when(…)). If you want to have exhaustiveness checks for when-statements, you might want to check out this article, where some workarounds are discussed (also read the comment section).

But personally, I would also prefer exhaustiveness checks for when-statements. The workarounds mentioned in the article are just that, workarounds.