Finding the first item in a sequence by type requires an extra cast to that type

Given a function that returns a sequence of a superclass/interface, such as [1]:

fun PsiElement.descendants(): Sequence<PsiElement> {
    return PsiElementWalker(firstChild, PsiElement::getFirstChild).asSequence()
}

using first/firstOrNull to find an element of a given class:

val module: PsiElement? = children().firstOrNull { e -> e is XQueryModule }
val versionDecl: XQueryVersionDecl? = module?.descendants()?.firstOrNull { e -> e is XQueryVersionDecl } as? XQueryVersionDecl

requires an additional cast (as seen in the versionDecl example). That is, expressions of the following forms require an extra cast:

val item: T = items.first { e -> e is T } as T
val item: T? = items.firstOrNull { e -> e is T } as? T

That is, the as is redundant due to the is check. It would be useful if Kotlin could infer the type and avoid the duplicated cast. This would be generally applicable to any predicate that checks for a type. This would then support the following:

val item: T = items.first { e -> e is T }
val item: T? = items.firstOrNull { e -> e is T }

In Java, I can do this by using things like:

public interface Sequence<A> {
    <B extends A> B first(Predicate<A> matcher);

    default <B extends A> B first(Class<B> c) {
        return findFirst(c::isInstance);
    }
}

and use:

T item = items.first(T.class)

[1] https://github.com/rhdunn/xquery-intellij-plugin/blob/master/src/main/java/uk/co/reecedunn/intellij/plugin/core/extensions/PsiElement.kt

You are trying to do 2 things with 1 operation: filtering and mapping. Unless you write a custom operator for that, I think it cannot be done with 1 operator.

Kotlin can correctly infer types if you do it like this:

val item: T = items.map { e -> e as? T }.filterNotNull().first()
val item: T? = items.map { e -> e as? T }.filterNotNull().firstOrNull()

Actually there is a function filterIsInstance that is the “right” approach - and does both, more efficiently. You can use it as:

val items.filterIsInstance<T>().firstOrNull()

Of course this means T needs to be concrete, if you have a kclass object you can use that as well as parmeter (rather than using a type parameter).

2 Likes