Compile time reflection/type checks for class fields

Hi, I’m new to Koltin and I’m looking for a way to perform some compile-type checks that I’ve ben using in C++.

The simplest possible example is having these classes

class Foo(val f1: String)
class Bar(val f1: String)
class Baz(val f1: Boolean)

and I’d like to check that Foo::f1 and Bar::f1 and Baz::f1 use the same type at compile time.

I can write

fun <T : Any> compileCheck(a: KClass<T>, b: KClass<T>) { }

and when I use e.g. compileCheck(String::class, String::class) and compileCheck(String::class, Boolean::class) it works as excepted, i.e. the first is fine and the second fails to build.

But I don’t know how to pass Foo::f1 and Bar::f1 and Baz::f1 to compileCheck(). I can get KProperty but not KClass.

I’m sure the answer is obvious but I’ve been scratching my head and searching the internet for a while now.

This isn’t what we usually do in JVM languages. But if you really want to do this, then you can use a very similar code to your own:

fun <T> compileCheck(a: KProperty1<*, T>, b: KProperty1<*, T>) { }

Unfortunately, V param of KProperty1 is covariant, so above code will work without problems for conflicting types - it will infer T to Any. I believe right now there is no official way to prevent this behavior, but considering that you do “weird stuff” already, you can use a workaround here.

Create kotlin/internal/Annotations.kt file with this contents:

package kotlin.internal

@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
internal annotation class Exact

Use above annotation to disable upcasting of T:

fun <T> compileCheck(a: KProperty1<*, @Exact T>, b: KProperty1<*, @Exact T>) { }

Result:

fun main() {
    compileCheck(Foo::f1, Bar::f1) // ok
    compileCheck(Foo::f1, Baz::f1) // compile error
}
2 Likes

What would you need that for? Can you provide an example?

Thanks @broot, that did the trick!

It also helped my search for related questions and discussions – it seems like it’s not an uncommon request to check types at compile time. In my case I want my APIs to be as predictable as possible and enforcing types is one of the ways.

My example was a bit oversimplified, the actual function does a lot more in runtime and in non-Kotlin world I’ve been used to perform these type checks at compile time.

Essentially I have a function that takes two fields from two data structures and creates a mapping/link between them to automatically populate one from the other. Checking types at compile time makes it immediately obvious when types don’t match. I avoid using primitive types like ints and strings so a type check can detect a lot of problems as soon as they’re made.

In Java/Kotlin it is pretty uncommon that the source code checks itself for correctness. Correct me if I’m wrong, but C++ source code can contain “reading logic” like for example compiler directives. Kotlin can’t. Even if you are able to solve above problem, you are still very limited on what you can do.

One possible solution is to move your source code checks to unit tests where you can use almost any verification logic you need. It would fail a little later in the development cycle, but still quite early. Testing is pretty much a part of the building process in JVM world, so it is almost like a compile-time check.

Still, it is uncommon to use unit tests to check the compile type of something, but I personally did something similar once. I developed a library based heavily on generics and I wanted to make sure that I didn’t messed up its API: OptionalValueTest.kt. I created assertType() and compileType() utils to make sure that when using my library, it resolves to correct compile-time types.

Another solution, probably the best one, is to write a compiler plugin. It would verify exactly what you need, it would be fully compile-time and it could provide meaningful error messages. Of course, it would require much more work to implement.

Thanks for the details!

Yes, C++ can do a lot of stuff at compile time and things can get complicated but if library maintainers take time/effort to enforce API correctness at compile time, it makes users’ lives much easier. Without the compile time checks, maintainers could write runtime checks (although C++ reflection is not always available), and let the user write unit/integration tests to make sure the library is called correctly.

In my case I want to catch as many incorrect cases as possible, there’ll be a lot of mapping and even though unit test should catch them, not being able to even compile an incorrect program is a big productivity boost in my case. No need to test impossible cases.

I’d want to avoid writing compiler plugin. :slight_smile: I was thinking about having some linting plugin/rules to check correctness but first I wanted to try to make Kotlin enforce it.

Looking around I came across something similar, it might be that over time more people will be interested in enforcing correctness at compile time: https://youtrack.jetbrains.com/issue/KT-13198#focus=Comments-27-4849053.0-0

if library maintainers take time/effort to enforce API correctness at compile time, it makes users’ lives much easier

I’m not super familiar with C++, but I suspect that the checks you are trying to do might make sense in C++, but not make sense in JVM languages. You mention library maintainers, so I suspect one of your concerns is making sure that, as the library evolves, the API is backwards compatible so that code which uses an older version of your library can continue to function unchanged with new versions of your library.

My understanding is that in the C++, you can control the layout of objects in memory, which can allow you to for example cast pointers to have something like a void* type, and then as long as you know the layout of the object in memories, you could always cast to a custom struct or union type and recover the data out of the original fields. In that sense, you can do something like duck typing, and as a library maintainer this gives you some flexibility in the way you can evolve your data structures while maintaining the same API.

This concept does not exist on the JVM. JVM languages have no say in the layout of objects in memory, and so duck typing is not really possible at the bytecode level. Instead, the JVM leans much more towards nominal typing, where two types are only considered equal if they have the same name. There is still flexibility on JVM languages in evolving the data structures while maintaining backwards compatibility, because we can upgrade the JAR file that contains the definition of the named type (to, for example, suddenly have new fields on the type), and as long as all the relevant APIs still refer to the same name for the type they’re using, everything just works.

As such, it doesn’t make sense in JVM languages to make assertions on the structures of types (i.e. asserting that a given type has a given field) the way it might in C++, because “same structure” is not the mechanism through which types are considered equal and/or interoperable in JVM languages; only “same name”.

And then it doesn’t make much sense to write checks that APIs are using the same name of types from one version of an API to another, because accidentally changing the name of the type referenced in an API is simply not a mistake maintainers make in practice (i.e. maintainer’s don’t, in practice, accidentally change a function from taking an Foo to taking a Bar).

it might be that over time more people will be interested in enforcing correctness at compile time

Yes, the people designing languages for JVM tend to prefer static checks over dynamic checks. But I think you might need to tweak your idea of what is “correctness” actually means on the JVM (e.g. nominal vs duck typing).

As an analogy, a person might say that they have some C++ tooling to ensure that there are no circular #includes and they’re looking for something similar in Kotlin, because they care about correctness, circular #includes are clearly incorrect, and they think Kotlin folks should care about correctness too.

The response isn’t that “Kotlin doesn’t care about correctness”, but rather that it doesn’t have a concept of #include, and thus there is no motivation to write any such tooling.

1 Like

To cite one of our grand masters: “What are you trying to achieve?”

The problem is that this is sometimes different from what is proposed. Your example seems to have a simple solution by design:

interface F1Interface {
    val f1: String
}
class Foo(override val f1: String) : F1Interface
class Bar(override val f1: String) : F1Interface
class Baz1(val f1: Boolean) : F1Interface // compile error
class Baz2(override val f1: Boolean) : F1Interface // compile error

Usually the compiler should do all the type checking for you without having to interfer manually or create extra artificial check mechansism as you proposed. If it doesn’t, ask yourself whether you have designed your types as you really intended.

This doesn’t mean that this will always be enough. To check architectural decisions which you cannot model via the type system, there are frameworks you can use to enforce these in tests. See for example ArchUnit. But I don’t know how well that works with Kotlin.

1 Like

Thank you for the comments, I’ve re-read them a couple of times and I think that we’re talking about somewhat different things.

To reiterate what I want to solve: In this instance I care about the correct use of my public functions, nothing else.

What works now: I can write fun foo(i: Integer) which a lot safer/correct/expressive than writing fun foo(i: Any) and then checking that i is an integer at runtime, documenting function foo that the caller must only call it with an integer and writing tests both for the foo() as the maintainer and writing tests for using foo() as the caller. In case of fun foo(i: Integer) nobody needs to test what happens when a String is passed in since it’s impossible and the function can only be used correctly (from type perspective).

What I want to achieve: write fun foo(i: Integer, property: KProperty1<*, Integer>) in a generic way so that when I call foo(42, Bar::field) it’ll only compile when Bar::field is an Integer (or a subtype of integer). More advanced version of foo() would be taking 2 KProperty's when the type of one can be assigned to the type of the second one.

If the first example is widely accepted as good, the second example is not far from it, it’s just enforcing type safety. I appreciate that historically there wasn’t much need for this as it wasn’t possible the best/common practices have evolved that way and my use case has been solved at runtime or using code generation (e.g. in ORMs).

My second example is something that’s already used internally in Kotlin standard library using internal Kotlin annotations that are not exposed (yet?) and by the sound of it other users are interested in using such functionality as well (e.g. my jetbrains link in a post above.)

I think everyone understands why fun foo(i: Integer) is “better” than fun foo(i: Any). Also, in order to implement some functionality we sometimes would like to affect/restrict Kotlin type system with things like @Exact, @OnlyInputTypes, etc. This is a normal thing and you don’t need to convince anyone that it is useful.

People are concerned here about a different thing. That you want to use these features to write compile-time tests of your own code. So this is like first defining foo as Int and then checking that foo is Int. It basically doubles the code without any direct benefits.

Note that in this @OnlyInputTypes issue linked by you, people didn’t want to use this feature in the way how you do. They need it to create an API and restrict how it could be used by others. Stdlib uses these annotations for similar reasons. On the other hand, you try to test your own code. But maybe I misunderstood you.

Thank you rephrasing. Just to confirm:

People are concerned here about a different thing. That you want to use these features to write compile-time tests of your own code. So this is like first defining foo as Int and then checking that foo is Int . It basically doubles the code without any direct benefits.

I never intended to use compile-time tests unless we start calling type safety in Any vs Int example “compile-time tests”. :slight_smile:

Note that in this @OnlyInputTypes issue linked by you, people didn’t want to use this feature in the way how you do. They need it to create an API and restrict how it could be used by others. Stdlib uses these annotations for similar reasons. On the other hand, you try to test your own code. But maybe I misunderstood you.

This is exactly my use case, to restrict how the API is used. With @Exact and @OnlyInputTypes I can introduce such restrictions so that only meaningful combinations of types can be used. It’s really no different that my Any vs Int example – I just control which inputs are meaningful.

Ok, maybe this compileCheck() example confused me, because I’ve got an impression that its only purpose was to check compile types of the rest of the code.

But if you implement some kind of functionality that uses generics and you feel that by default Kotlin allows to do “too much” with the API of your library due to automatic casting and/or type inference, etc. then I fully agree with you. These are valid cases for features like @Exact , @OnlyInputTypes. It is a missing feature in Kotlin right now.

I agree, it wasn’t the best example to describe the problem. My follow-up comment tried to clarify what’s happening and that the function does a lot more than checking types.