Can I have (/is there already) an annotation `Dont...
# language-proposals
g
Can I have (/is there already) an annotation
DontInferAny
so that I can avoid a call to
Collection.minus<T>(it: T)
inferring
T
is
Any
? I used to have this code
Copy code
data class SimpleDomainValueType(name: String);
data class Customer(simples: List<SimpleDomainValueType>);

fun businessLogic(cust: Customer){
  val specialSimples: List<SimpleDomainValueType> = doBusinessLogic()

  val result = specialSimples- cust.simples
}
Then I did a refactor
Copy code
data class SimpleDomainValueType(name: String);
class SimpleDomainValueCollection {
  operator fun get(index: Int): SimpleDomainValueType = ...
  val size: Int get() = ...
}
data class Customer(simples: SimpleDomainValueCollection);
And I spent 30 minutes tracking down that the line
val result = specialSauceItems - cust.simples
still compiles, because
minus
is effectively becoming
Copy code
val result = specialSimples.minusElement<Any>(cust.simples)
which is definitely not what I want. I suspect its not what the overwhelming majority of people want. I would think we could tell kotlin something like:
Copy code
public operator fun <@DontInferAny T> Iterable<T>.minus(elements: Iterable<T>): List<T>
to tell kotlin that
Any
is not an acceptable resolution for
T
here. --- this would blow up a lot of existing code, especially people using raw-typed lists or similar 🙃 but it makes my life easier and thats all that matters so i want it.
j
Chances are, if this were implemented, we would soon come back with a need for
@DontInferComparable
, and probably half a dozen more. A more interesting question for me is, how do you use this resulting list? Didn't you get a compile error further down the line anyway? Having a
List<Any>
is not really a problem in itself. Most likely the problem comes when you do stuff with it, so you would quite quickly get a wrong type somewhere.
g
yeah I was thinking the same thing
DontInferSerializable.
And thats a good point, the fact that the inferred variable type for
result
is now
List<Any>
instead of
List<DomainType>
would likely induce a compiler error most times. In my specific case it gets passed into a kind of string renderer that isn't doing a strong type check, and it is inducing the cryptic runtime error that i tracked down for 30 minutes
y
I think the internal annotation
OnlyInputTypes
does what you want.
🤔 1
j
There's also
@NoInfer
which is nice https://youtrack.jetbrains.com/issue/KT-54642
Not what you want for this case, you want
@OnlyInputTypes
, but it's in the same realm
👌 1
Yeah, it's in the same space of reducing problems from accidental inference (replying to your deleted message here).
g
Sorry yes, my deleted message was that
@NoInfer
on
operator fun <@NoInfer T> Collection.minus(elem: T)
would mean that every invocation of minus requires an explicit type which would mean you could never use it as an operator, only as a method (IE you could no longer write
things - elem
you would be forced to write
things.minus<DomainType>(elem)
)
But in other situations @NoInfer is helpful. last time I needed it it was still not publicly exposed
j
Yeah neither are, sadly. Hopefully we get a set of tools/annotations in this space some time in the post-2.0 world.
g
How about
@DontInfer(Any::class)
? My very limited time writing K2 compiler plugins makes me think this functionality could be implemented as a K2 plugin (But the validation would only work if the compiler plugin was included, otherwise there would be no validation and the annotation would be ignored.
On a side note, the disambiguation problems with collection operation overloads has pushed me away from defining them with parameters that have a collection and its element type. (I'm writing this as I'm actively falling asleep in bed, hopefully this all makes sense still)
m
While
OnlyInputTypes
and other internal annotations should be productized, we definitely have room for improvement inside our stdlib by annotating—with softer rules—required functions that greatly rely on elements' equality. Speaking of the
NoInfer
annotation—from my perspective, it mostly makes sense for functions that infer their types based only on the expected type. This might be especially important for reified functions that perform some casts inside their bodies, and an 'implicit' change of the inferred type can have some unexpected consequences
Copy code
// it'd be better to have NoInfer annotation to always specify type arguments explicitly
inline fun <reified T> Any.deserializeLike(): T {
    return this as T
}

foo(x.deserializeLike()) // deserializeLike depends on foo, not on x 
bar(x.deserializeLike()) // deserializeLike depends on bar, not on x
âž• 1
g
I think @Joffrey’s point about type inference is really important here. Type inference always introduces some room for problems like mine. consider a similar case without generics
Copy code
val inferred = some-expression

doStuff(inferred)
A core assumption behind type inference is that if `some-expression`'s return type changes, the compiler will introduce sufficient errors further down without the need for the user to explicitly denote the type when writing
val inferred
. In my specific case that didn't happen, the kotlin compiler did a reasonable job, i think, in changing the type of
result
from
List<DomainType>
to
List<Any>
, but the problem for me was that
List<Any>
was actually a sufficiently close-enough type to avoid causing a compile time problem, instead introducing bizarre runtime problems. I guess what I'm advocating for, (in an already built system thats difficult to change), is that type inference be generous when inferring non-generic types and much more whine-ee when inferring generic types. At the very least I would think IDEA can have a "suspicious type inference" warning anytime it sees something like
List<Any>
as an inferred type; in most instances I'd wager that inference is wrong.
c
Reminds me somewhat of TypeScript’s
noImplicitAny
, in the sense that you must be explicit