Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Generic Algebraic Data Types require `T` on member types?

I want to define a Generic Algebraic Data Type for use with my parse function like this:

sealed class Result<T> {
    class Success(val value: T, val pos: Int) : Result<T>()
    class Failure(val message: String, val pos: Int) : Result<T>()
}

fun <T> parse(t: Parser<T>, input: String, initialPos: Int = 0, collectErrors: Boolean = true) : Result<T> {

However this is not allowed as T is then an undefined reference.

If I add T to all member types it works:

sealed class Result<T> {
    class Success<T>(val value: T, val pos: Int) : Result<T>()
    class Failure<T>(val message: String, val pos: Int) : Result<T>()
}

To me this is somewhat confusing which makes me believe I am missing something here. Why isn't T seen when defining the member types in the first case?

In addition when creating an instance of Success I would expect the syntax to be:

Result<T>.Success<T>(tv.someValue, pos)

But that won't work instead I do this:

Result.Success<T>(tv.someValue, pos)

This is the preferable syntax to me but I am struggling to understand why I should leave out T on Result here.

like image 892
Just another metaprogrammer Avatar asked Dec 18 '22 15:12

Just another metaprogrammer


2 Answers

Result is a generic class, with a single generic parameter named T. The class name is Result though, not Result<T>.

Success is a generic class, too. So, since it's generic, you need to define it as Success<T>. If you don't, then it's not generic anymore. Note that, even though it's a subclass of Result which is generic, it could be a non-generic type. For example:

class Success(val value: String, val pos: Int) : Result<String>()

Also note that although Result and Failure are generic, they don't use their generic type for anything. So you could in fact define your classes as

sealed class Result {
    class Success<T>(val value: T, val pos: Int) : Result()
    class Failure(val message: String, val pos: Int) : Result()
}

Now, why do you need to use Result.Success<T>(tv.someValue, pos) and not Result<T>.Success<T>(tv.someValue, pos)?

Because the name of the class is Result.Success. The parameter type is not part of the class name. Most of the time, it's not necessary to specify it at all because it will be inferred:

val r = Result.Success("foo", 1)

creates an instance of Success<String>. If you wanted instead to create a Success<CharSequence>, then you would have to specify the generic type explicitly:

val r = Result.Success<CharSequence>("foo", 1)

or

val r: Result.Success<CharSequence> = Result.Success("foo", 1)
like image 116
JB Nizet Avatar answered May 23 '23 08:05

JB Nizet


The rules are the same as in Java. It basicly comes down to Success and Failure being static nested classes of Result. There are no "member types" in Kotlin, static nested classes are regular classes that have access to the scope of the outer class. And if a class extends a generic super class, it always needs to bind the generic type parameters.

In contrast, non-static nested classes (denoted by the inner keyword) always carry the generic type paremeter of the outer class. This way you can construct the following type hierarchy:

open class Foo<T> {
    inner class Bar : Foo<T>()
}

To instantiate Bar you will need to have an instance of Foo:

val b = Foo<String>().Bar()
like image 42
Kirill Rakhman Avatar answered May 23 '23 07:05

Kirill Rakhman