Bug in `where`?

Hopefully this is the right place for this, and not over in Language Design. If I should x-post, let me know!

I love the ifEmpty and ifBlank extension functions for CharSequence (mostly used with String for me), but found a need to combine them. So I wrote the following:

inline fun <C, R> C.ifEmptyOrBlank(defaultValue: () -> R): R where C : CharSequence, C : R =
    if (isEmpty() || isBlank()) defaultValue() else this

This function signature is exactly the same as both ifEmpty and ifBlank, but for some reason I’m getting a complaint from the JVM compiler saying:

Type parameter cannot have any other bounds if it’s bounded by another type parameter

Is this a bug with where, or is it “works as intended”? If it is the latter how does it work for the standard library, but not for others?

Any insight is appreciated!

For reference:
SDK: Azul Zulu 11.0.14
Language Level: 11
Kotlin Version: 1.5.32
IntelliJ Version: 2021.3.2 (CE)
Kotlin Plugin Version: 213-1.5.10-release-949-IJ6777.52

1 Like

This subtyping relation doesn’t work well with Java-compatibility for instance and can be a bit buggy at times. The Kotlin compiler allows it for the stdlib because it is useful in a minority of scenarioes. If you would like to use it yourself, just suppress the error :slight_smile: :

@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER")
inline fun <C, R> C.ifEmptyOrBlank(defaultValue: () -> R): R where C : CharSequence, C : R =
    if (isEmpty() || isBlank()) defaultValue() else this
4 Likes

Unfortunately, sometimes stdlib “cheats” and does what is normally impossible in Kotlin. This is usually done by utilizing magic internal annotations that adjust the way how the Kotlin compiler works or that enable hidden features. Most notably, stdlib uses @NoInfer and @Exact annotations and in your specific case it is: @InlineOnly.

If you add @InlineOnly annotation to your project like this:

package kotlin.internal

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.BINARY)
internal annotation class InlineOnly

And then annotate your function with it, then the error will go away. But this is just a hack to confirm why stdlib code compiles fine. We should probably not do this in the real application.

4 Likes

Specifically by the way, the rationale behind allowing this for InlineOnly is that that annotation, as the name suggests, makes it so the function can only be called wherever it can be inlined, and so it doesn’t exist in the resulting JVM bytecode. Because of that, Java compatibility with how those weird type parameters are resolved does not matter, and so the compiler happily obliges and lets you define such a where clause

1 Like

Excellent explanations, thank you both! I agree that adding @InlineOnly in my application, as @broot suggested, is probably not wise. I think that @Suppress is a little cleaner in this regard since it explicitly signals I’m suppressing a compiler complaint. Thanks @kyay10 for providing the exact diagnostic name!

I suppose I could scrap all of this and do something like ifEmpty { "default" }.ifBlank { "default" }, but that feels a little clunky, and might be confusing to others who read it (future “me” included).

1 Like

I’m not at a computer to test it, but I’m pretty sure isBlank returns true for empty strings, so does exactly what you want

4 Likes

Oh wow, yeah looks like you’re right. I would have bet isBlank was specifically non-zero length strings comprised of only whitespace characters, but according to the docs:

[isBlank] Returns true if this string is empty or consists solely of whitespace characters.

1 Like