-
Notifications
You must be signed in to change notification settings - Fork 368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Type-checking of reified types #35
base: master
Are you sure you want to change the base?
Conversation
I think we need more use cases for this. So far, it's not clear how relevant this feature might be. |
To be clear: this feature has rather far-fetching consequences:
So, to decide something about this proposal, we need to collect use cases and evaluate the pros and cons very carefully. |
Do you think that any function relies on this behavior? The current behavior feels more like a bug and programs will just run better after doing this change.
I cannot estimate how much effort this is. I don't know exactly how Kotlin is implemented and how byte code is generated (perhaps using ASM?) Doing this inlining in the AST (or whatever intermediate data structure you use when you parse Kotlin) before it is generated to byte code is not an option or very complicated?
The behavior just changes for reified type parameters and it changes to the same behavior as it does for all other types in inlined functions. IMO being able to check with The Kotlin documentation currently reads:
-- https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters So, the more the type parameter variables typed with it behave like normal classes, the more it conforms to the expected semantics, doesn't it?
As I described above literally any use case for reified type parameters, like constructing the value and then executing some overriden initialization logic on it, is a use case for this proposal. |
A short follow-up: I can return a value of type (this as T) copyTo (copyAction as T) So this is not surprising as I know that you do the dispatch during byte code generation before, but still it is a counter-intuitive and inconsequent behavior. |
@juangamnik Could you clarify in the proposal, what scope is used for resolving method calls for reified types in inlined function: either it's the scope of inline function declaration, or it's the scope of the function call-site? Could you explore, whether overload resolution conflicts become possible during inlining? How these conflicts should be reported? |
@ilya-g perhaps I misunderstood you, but do you want me to describe the current situation in Kotlin or the intended situation (of the proposal)? Because if you mean the former (current situation), the answers to my issue in YouTrack suggest that it is declaration site since the byte code of the function body is copied as is. But since I have no insights (yet) to the Kotlin code, this is 2nd-hand-knowledge and it might be wrong... TL;DR I do not understand, what you exactly advice me to do 😉 |
As the proposal is going to change the overload resolution, the indented behavior of overload resolution should be described in the proposal. I'll clarify the terms, so that my question is to be clear.
So the question is which of these two scopes should be used to resolve methods calls for reified types in inline functions? |
I knew that, but better you say me something that I know, than missing something that would lead to confusion 😉.
Ah now I know what you mean. If the scope for available symbols could be declaration-site that would be great, because then I could call stuff in an inline method, that possible is not available at call-site (private methods) and it would behave more like a normal method definition. But I think this is out of scope of this proposal. So this leaves call-site scope for the moment, right. And you would like me to elaborate on this and the possible conflict situations that may arise and how to handle them? If this is what you propose (please give me a short nod or something), I can target this, but I think it will be just an explanation why this does not apply to this proposal:
Do you agree? I have another proposal in mind which might be fixed directly with this one regarding smart casts for reified type parameters. Currently, if you smart cast a reified type parameter(e.g. in a But for both cases I think the scope is irrelevant. Do you agree? |
To be clear about this: I would like this very much, but I am aware, that this is nearly impossible when inlining, because after copying the code, private methods from declaration-site will just not be available (so it is a "security" issue). On Slack language-proposals channel I proposed two more things, which I do not know whether they will be appreciated:
I think all these 4 proposals have a common ground. Perhaps I should put all these ideas into the proposal? I didn't do that in the first place, because I was unsure, whether this would be appreciated (I wanted to start slowly 😄) |
okay, lets stick with that option and expore it in detail. Consider the following example:
If you call |
It should happen exactly the same as if I had written this without an inline function: // see above...
fun test() {
C().foo()
} The compiler will inline this and should then throw the error, that the inline function contains a call, which is ambiguous. So this is currently OK: fun test() {
(C() as A).foo()
(C() as A).foo()
} And so this should be the behavior for the inline function: fun test() {
// compiler error
invokeFooOn(C())
// everything OK
invokeFooOn(C() as A)
// compiler error (because the upper bound is A and !(B: A)
invokeFooOn(C() as B)
} Should I add this example to the proposal? |
definitely, it would be valuable.
That's where things become seeming weird: thus to use such an inline function one has to know its inner workings, its source, otherwise coping with such errors is going to be based on guesswork. And the source of the function might be not available at hand. |
But the code won't compile without the source at hand. The compiler must have this information to do the inlining. Or is this done at runtime via byte-code weaving like in AOP? And if he has the information he can moan. Of course, the error message has to be self-explanatory, but since the compiler has all the info... |
Currently the source code isn't required to use inline functions. Inlining is performed with byte-code transformation techniques. |
At runtime? |
No. Inline functions are inlined at compile-time. |
So the information should be there to produce the compiler error and to do the "downcast", shouldn't it? For the inlining at compile time you need the source code 😄 But the downcast won't easily be done as a byte code transformation since complete method calls have to be (re)looked up and replaced. But perhaps this can be done with the annotations you mentioned that help to pinpoint these instructions and to replace them? |
Sorry this is a bit off-topic, but I tried this code fiddling around with the example of @ilya-g: // inline function declaration-site
interface A
fun A.foo() {
print("a")
}
inline fun <reified T : A> invokeFooOn(instance: T) = instance.foo()
// inline function call-site
interface B
fun B.foo() {
print("b")
}
class C : A, B {
fun foo() {
print("c")
}
}
fun test() {
C().foo()
(C() as B).foo()
invokeFooOn(C())
} This prints out |
If class A { }
class B : A { }
fun A.foo() = ...
fun B.foo() = ...
//...
val a = A()
(a as dynamic).foo() --assuming of course that the Does anybody know the rationale for why the C# guys chose not to implement polymorphic dispatch for extension methods? It might be valuable here. I'm looking for a fabulous adevntures in coding post but cant find one. Looking at swift extensions, they appear statically-bound as well. I wonder also what kinds of limitations this feature puts on type classes or other forms of ad-hoc polymorphism. The requirement that all reified extension functions must polymorphically dispatch might be difficult/impossible to maintain if/when kotlin starts getting into funny types. Another question I have is about generics and erasure: class A {}
class B : A {}
List<A>.foo() = ...
List<B>.foo() = ...
val bees : List<B> = listOf(B(), B())
val cast = bees as List<A> //assume kotlin.collections.List not java.util.List
cast.foo() erasure ensures the only thing the compiler can hit is class List<T: Any>{
fun <TA> foo(a: TA) //...
fun <TB> foo(b: TB) //error!
} thus, if we did add this functionality, I feel like the compiler --or rather IDEA-- would need some sophisticated "this code looks suspicious" analysis to inform the programmer what parts of variables receiver type are being bound statically (because of erasure) and what types are being bound dynamically (polymorphically). Still, I'm with juangamnik in spirit, it seems odd that extension methods dont do anything clever with respect to overloads. |
I know that extension functions use static dispatch, but since
Xtend uses static dispatch for extension methods, but offers the Further on, Xtend allows only to dispatch all parameters or none. But I think it would be great (in Kotlin too) to be able to add
Sorry to open the box of pandora here. I did not intend to change this proposal to how extension functions and dynamic dispatching could work. As @ilya-g said, we should focus on the dispatching of reified type parameters in inline functions. |
@ilya-g I will then update the proposal according to our discussion and add this example, ok? But this may take a day or two, since I'm low on (spare) time this weekend. |
edit: sorry I misread your comment... but yeah, this still seems correct to me, for the same reasons that The result of the expression Did you perhaps mean to rewrite interface
? this will result you getting "ccc" (assuming you add an |
Somewhere in the documentation IMHO I read that extension methods should never be able to override instance methods. Since you just cast The only solution that comes to my mind is to do a test whether an instance method exists and then do a dynamic call (in byte code) and if not do the static call. But it is not very efficient. So lets stick to this proposal from now on. Thanks @Groostav 😄 |
It is a hard constraint that the language evolves in a backwards-compatible way. This proposal will alter behavior of potentially a lot of existing code silently. We'd need very strong reasons to do that. The claim that some behavior is "just better" than some other is an emotional statement :) Let's try to stick to technical terms. Present behavior is well-defined. It's not better or worse than any other well-defined behavior, because "better" and "worse" do not apply at this stage of language design. If we were talking about a new language feature we could maybe see what feels more natural or serves more use cases (those are arguably reasonable definitions of "better"), but at this point such reasoning is not applicable.
It can be implemented, just very complicated. Plus, it makes more work for the type-checker, because such inline functions must be type checked on every application as opposed to once per declaration.
Your comment does not refute my point, which is: reified or not, the code you read in a body of inline function does not type-check in place any more. BTW, do you propose to type-check it once per declaration + per every call site, or only on call sites? |
OK I didn't meant it in an emotional way, but that it is more consistent over the complete language (so less surprises on developer site) and more object-oriented. Of course, this is not completely objective. I saw the documentation of Kotlin saying "behaves mostly like a non-generic type" and this is why I expected, that the more it behaves like a non-generic type the more conform is it to the "specification" (I don't know whether you have some kind of language specification like Java has, where this behavior/semantics is described). So, sorry about that.
Got your point 😄 .
(this as T) copyTo (copyAction as T) I hope this does not sound emotional again, but a cast should lead to a type check for reified generics shouldn't it? Why bother casting it in the first place? Just for returning a value? This would be somewhat arbitrary (technically 😄).
I'm not fully confident, whether I oversee all the consequences, but "only on call sites" should suffice shouldn't it? A last point: If one could add the keyword |
@abreslav If I should not drop the proposal 😉 should I rename it to "type-checking of reified types"? Since if you call an instance method on an instance of a reified type it is dynamically dispatched... So the title is currently just wrong. |
@juangamnik Do you mean this documentation: https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters? The link to reified type parameter specification is given in the docs, here it is: https://github.com/JetBrains/kotlin/blob/master/spec-docs/reified-type-parameters.md Maybe, we should fix then the documentation instead, in order not to convey false expectations? (cc: @yole) At the moment reified type parameters behave as one could have expected them based on, say, C# experience where generics are reified. |
Ok, if we don't want to break too much, this is in fact "duck typing for reified parameters". NB1: it also covers leaking abstractions issue mentioned above. Note that in Kotlin As of 1.1, calls with reified type parameters involved are resolved statically exactly as it happens for any other type parameters. Now, we should specify exactly what kind of operation Example (fake syntax):
|
Yes. The documentation there says:
The referenced low-level description does not contain any information on how the type checking works or did I miss this? |
My proposal to use the abstract syntax tree of inline functions (instead of byte code), with placeholders (I don't have a better name) for "statements using Of course this does not help for leaking abstractions (you refer to @ilya-g 's example where ambiguous calls have to lead to an error on call-site. But IMHO this is exactly what I would expect. If I want reified types and I make an ambiguous call the compiler should prove me wrong 😉.
Hu, this is IMHO a heavyweight solution for this. So you propose to use to "push" the extension function into some kind of explicitly defined type constraint, so that it is some kind of member extension function which leads to a virtual dispatch on the implicit receiver? How would the different implementations of typeconstraint CopyableAction<T> {
constructor T()
fun T.copyTo(action: T)
}
// should implementations be done like this?
typeconstraint CopyableTemporalAction: CopyableAction<TemporalAction> {
fun TemporalAction.copyTo(action: TemporalAction) {
// do the copy stuff
}
}
// more concrete implementations...
inline fun <reified T> T.copy(): T where CopyableAction<T> {
val copy = T()
copyTo(copy)
return copy
} |
@juangamnik
|
OK when I saw your code I thought already, that this can be done with an implicit receiver. So I don't get the point of your proposal with an explicit type constraint. The pro I see with your approach is, that I don't have to use the copyable explicitly. But in both cases I have to rebuild the inheritance of the original class hierarchy I want to extend. The same holds for a |
Still can be done with more crafty approach to extension provider hierarchy (which can be considered boiler-plate, though). |
It is not only boiler-plate it is redundant and error-prone or do I get the meaning of your sentence wrong? IIRC Kotlin has been created because Java misses some language features and abstractions that make a programmers life easier as well as to overcome some bad design decisions that have been done because backward compatibility was the prime directive. IMHO solutions where I have to "copy the inheritance relation" just because I cannot convince the language to do the resolution in an object-oriented fashion were the initial driver for Jetbrains to create their own language (and because Scalas compiler was to slow). It is a common problem of many object-oriented languages that adding functionality to an existing type is a pain (leading to visitor pattern, ...) and extension methods is a nice addition to overcome this situation, but the way they are currently realized in Kotlin (as much as I like the language) I always have the feeling that I have to break my fingers. |
Indeed. That's especially true since you've mentioned Scala compilation speed. Kotlin stays away from some features for a reason. So far, what we have:
Can you think of something that will fit into these constraints :) ? |
I have no objections |
Adding an explicit fun dispatch SomeType.foo() { /* ... */ } I would do it more generally to allow something like this: // first param is dynamically dispatched, second not.
fun foo(dispatch a: A, b: B) Further on, I would allow to define a complete method where all parameters are dynamically dispatched: // first and second parameter are dynamically dispatched.
dispatch foo(a: A, b: B Unfortunately, unlike in Xtend the extension receiver is written with dot-notation, so that this distinction is a bit subtle: // extension receiver is dynamically dispatched, parameter not.
fun dispatch SomeType.foo(a: A) { /* ... */ }
// extension receiver and parameter are dynamically dispatched
dispatch fun SomeType.foo(a: A) { /* ... */ } This would allow to explicitly decide where dynamic dispatching should be used, without the need to implement it again and again. Of course it would be nice if the interface TemporalAction
interface MoveAction: TemporalAction
dispatch fun TemporalAction.copyTo(action: TemporalAction) {
// do the copy stuff
}
dispatch fun MoveAction.copyTo(action: MoveAction) {
// should call `TemporalAction.copyTo(TemporalAction)`
super.copyTo(action)
// do the copy stuff for moving
}
// more concrete implementations...
inline fun <reified T> T.copy(): T {
val copy = T::class.java.getConstructor().newInstance()
// this call is dispatched, because of the `dispatch` keyword in the implementations.
copyTo(copy)
return copy
} Like What do you think? |
Sorry, but does that implicitly mean I should not drop this proposal? Following @dnpetrov the proposal could change drastically because there are some constraints to Kotlin's evolution (which you mentioned before too). I tried to address these constraints in my last comments here (introducing |
@juangamnik It's up to you to decide whether to drop/abandon the proposal. My estimate so far is that it's neither easy to implement, nor a very high priority. As with any other proposal, you (and others) can work on it and refine it to a more or less doable state, and starting from there I can't predict whether it will be finally accepted (and when). It's a question of a number of factors, including:
If I'm not mistaken, this looks a lot like multimethods. This is a very well-studied feature which is known to cause a lot of headache implementation-wide and for one thing has very unpleasant interactions with overloading. (And it's likely a lot more work than even what you proposed initially.) I'm very sceptical about this direction. But I'm reluctant to closing the door on any idea, who knows: maybe you can refine it so that it's great for Kotlin. Having said that, I expect it to be a lot of work on your side before this proposal becomes viable. |
Yes it is about multiple dispatch/multimethods. A lot of languages have this feature and like with many advanced features, it is not trivial to implement them and the semantics have to be chosen carefully. I will do a proposal for that as soon as I have some time and tranquillity (I hope I use this word right 😉) to do it right.
I would like to give a try to implement some of my proposals (at some point), but I need a crash course in Kotlin-development. Is there some documentation about the architecture, build-management, and how to contribute to Kotlin-code? |
@juangamnik Not very much, unfortunately. The Readme file has basic build/setup instructions. This page has some info, also there's the |
…-reified-types.md Updated proposal with respect to discussions on Kotlin#35
I updated this proposal regarding the discussions. Please could someone add @dnpetrov as shepherd as discussed on Slack? I will add another proposal for dynamic dispatch of extension receivers using an explicit keyword like I think this proposal is valuable as it is now, since the conflict resolution does not leak abstractions more than the already existing constraint that calling private methods in the function body of inline functions is not allowed at declaration-site (because they would not be visible at call-site where the body is copied to). Further on, IMHO this behavior change is not breaking existing source, but fixing it, since the "new" behavior respects the reified type. Compelling argument is IMO the following inconsistency:
Could you please reevaluate, whether the proposal reflects the discussions here and whether it fits into the constraints @dnpetrov mentioned? |
I started a new proposal for dynamic dispatch of extension receivers as discussed with @dnpetrov here. He volunteered to shepherd this, so perhaps someone assigns the pull request to him? I will start separate proposals for reified generics in non-inline functions (as discussed on language-proposals on slack via implicit |
How should we handle ambiguities?Assume you have 2 or more override-equivalent implementations of a particular function with "dispatch" receiver, possibly in different compilation units.
Are local "overrides" allowed for functions with "dispatch" receiver?E.g.,
|
@dnpetrov
I would treat both cases the same. If you import an extension from another compilation unit and get an ambiguity then, I would raise an compilation error in the importing class. If you mean with at run-time that an existing extension method in one compilation should not be "overriden" by a definition in another compilation unit during call resolution I totally agree.
As a quick shot answer I would say yes. But I am unsure whether this has major implications. |
Oops, no, that's just me. Ok, let's continue under #46. |
Added section Realization. Added discussion on relation to "overriding extension methods"
@abreslav gave me a "go for it" in:
https://youtrack.jetbrains.com/issue/KT-12897#comment=27-1519928