Chris Schmitz
Chris Schmitz

Reputation: 20980

how to read kotlin type annotations

I'm coming to kotlin after working in mostly dynamically typed languages for years, so I get a lot of what I'm seeing, but I'm still tripping up a bit over reading some of the type annotations.

Most of them make sense (I've written some C++ and typescript so I'm not wholey familiar with more strictly type languages). so stuff like annotating the parameters and return types for functions, variable declaration, stuff like that makes sense.

What I'm having trouble with is the more complex annotations like looking at this explanation of the fold method when talking about higher order functions:

fun <T, R> Collection<T>.fold(
    initial: R,
    combine: (acc: R, nextElement: T) -> R
): R {
    var accumulator: R = initial
    for (element: T in this) {
        accumulator = combine(accumulator, element)
    }
    return accumulator
}

I get that:

And I can use it like this:

val greetings = listOf("hey", "hi", "yo", "what's up")

val personalized = greetings.fold("", { carry, current -> "${carry}\n$current, Chris." })

println(personalized)

That all makes sense, but what does the <T, R> between the fun and the Collection mean? What is that part called? (It's hard to search for an explanation when you don't know what the thing you're looking for is called :P)

And more importantly, is there a section of the documentation that specifically talks about how to read these annotations or what each are called? I've been looking through the docs and searching in general for an explanation of how to read the type annotations and I can't find anything.

It feels like a silly question, but to the uninitiated it's kind of daunting and the docs are written as if you already understand that part of the language.

Upvotes: 0

Views: 237

Answers (2)

Joffrey
Joffrey

Reputation: 37829

As Alexey already said, these names between angled brackets after the fun keyword are called "type parameters". They are used to declare generic functions.

the Collection refers to an arbitrary collection with elements that are of type T

Here you can see that Collection and T play different roles: Collection is a well-known defined type that you are referencing, while T is just a name that you arbitrarily choose for the definition of this function.

We want the compiler to check that Collection is a type that is defined and imported, and if you make a typo there will be a compile error. On the other hand, we don't want that for T and R, so it is necessary to mention them in a special syntactic place so that the compiler knows you're just making up arbitrary names for the sake of the function definition.

It is nice to draw a parallel between the type parameters and the method arguments. The method arguments are also arbitrary names that you define in the signature and use in the function body, as opposed to class members like properties, which you can access without declaring them as arguments.

Just like the values of the arguments are passed when you call a method, and can be different for each different invocation, the "values" of the type parameters are also given at the call site, and can be different for each invocation (they are often inferred, though, so you don't see them).

Note that the "value" of a type parameter is a type (e.g. String), not a value in the usual sense like the string "abc". You can actually specify these types explicitly on the call site if you want:

listOf(1, 2, 3).fold<Int, Int>(42) { acc, e -> acc + e }

The syntax on the call site is similar to the declaration site, it uses <>, except that it's written after the function name. In general, these types are easily inferred by the compiler using the argument types or the return type in the context of the call site, that's why it's often unnecessary to explicitly specify them.

Difference with generics at the class level

It may seem weird that the methods in the interface List don't need to declare such type parameters, despite the fact that they use generic types:

interface MutableList<T> {
   
   fun add(element: T): Boolean {
       //....
   }
}

This is because T is already "well-defined" when using it for the method declaration: it was already defined as a type parameter for the List interface itself. The mechanism is the same, but the difference is the scope of the definition: class-level type parameters are defined by the instance of the class (you can create a List<Int> or a List<String>, and this is chosen when you create your instance), while function type parameters are defined by each call to the function.

You can even combine both:

interface List<T> {
   
   fun <R> map(transform: (T) -> R): List<R> {
       //...
   }
}

Here T will be determined by the list instance on which you call map, but R can be different for each call to map even on the same list instance.

Upvotes: 1

Alexey Romanov
Alexey Romanov

Reputation: 170899

<T, R> are the type parameters. Since you are familiar with C++, it's like

template <typename T, typename R>

It just happens to be placed after the fun keyword in Kotlin (and after the type name when declaring a generic class/interface/type alias) instead of before the function definition.

Upvotes: 0

Related Questions