-
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
Create override-extension-functions.md #46
base: master
Are you sure you want to change the base?
Conversation
Currently it's all about syntax, which is definitely not enough. Should "dynamic dispatch" work for receivers of How exactly should "dynamic dispatch" work? (you can skip implementation details here, for now just describe the intended behavior) |
No, for all explicit extension receivers. I thought that the example code already illustrates that, doesn't it?
I will elaborate on this topic. Is more code examples (using the
Sure. |
Can you describe in detail how the dispatch should be performed?
At run-time, you also have the following definitions in module M2 (depending on M1):
Now, some code in M1 receives a value with run-time type T <: IB. |
Yes. I was on vacation I need to find some spare time. I will include this (and the last question/answer on #35) into the proposal. A quick answer to your questions:
Yes. It should just behave as if the extension property/method has been defined as a property/method on the
In your example // module M1
package m1
import m2.foo
interface IA
fun (dispatch IA).foo() { ... }
// some function body in M1
val someBasA: IA = getSomeIB()
someBasA.foo() // executes `fun (dispatch IB).foo()`, since the method has been imported.
// end function body
// module M2
package m2
interface IB : IA
fun (dispatch IB).foo() { ... } |
So, it sounds more like an overloaded function resolved at call-site during inlining. |
No I don't think so. As I wrote in my answer it should work for normal non-inline functions, too. I don't think that this proposal should have anything to do with inline functions (although I use it in my example). But of course it is about overloading, since I cannot override outside the class (extension functions can only be overloaded iff they are not bound as a method to an implicit receiver), but overloading in the extension receiver (which is the first parameter in a extension function in languages like Xtend) is more or less "the same" as overriding member functions. Resolving overloaded functions at call-site is the path of #35. |
OK I think I know now why you mean this is a special form of overload. Consider the following snippet in Kotlin 1.0.3 (sorry it is long): open class IA {
fun foo(newObj: IA): IA {
println("IA.foo")
return newObj
}
fun foo(newObj: IB): IB {
println("IB.foo")
return newObj
}
fun foo(newObj: IC): IC {
println("IC.foo")
return newObj
}
fun bar(newObj: IA): IA {
println("IA.bar")
return newObj
}
}
open class IB: IA() {
fun bar(newObj: IB): IB {
println("IB.bar")
return newObj
}
}
open class IC: IB() {
fun bar(newObj: IC): IC {
println("IC.bar")
return newObj
}
}
fun IA.baz(newObj: IA): IA {
println("IA.baz")
return newObj
}
fun IA.baz(newObj: IB): IB {
println("IB.baz")
return newObj
}
fun IA.baz(newObj: IC): IC {
println("IC.baz")
return newObj
}
fun IA.bazz(newObj: IA): IA {
println("IA.bazz")
return newObj
}
fun IB.bazz(newObj: IB): IB {
println("IB.bazz")
return newObj
}
fun IC.bazz(newObj: IC): IC {
println("IC.bazz")
return newObj
}
fun main(args: Array<String>) {
val a: IA = IA()
val b: IA = IB()
val c: IA = IC()
a.foo(a)
b.foo(b)
c.foo(c)
println()
a.foo(IA())
b.foo(IB())
c.foo(IC())
println()
println()
a.bar(a)
b.bar(b)
c.bar(c)
println()
a.bar(IA())
b.bar(IB())
c.bar(IC())
println()
println()
a.baz(a)
b.baz(b)
c.baz(c)
println()
a.baz(IA())
b.baz(IB())
c.baz(IC())
println()
println()
a.bazz(a)
b.bazz(b)
c.bazz(c)
println()
a.bazz(IA())
b.bazz(IB())
c.bazz(IC())
} It outputs: IA.foo
IA.foo
IA.foo
IA.foo
IB.foo
IC.foo
IA.bar
IA.bar
IA.bar
IA.bar
IA.bar
IA.bar
IA.baz
IA.baz
IA.baz
IA.baz
IB.baz
IC.baz
IA.bazz
IA.bazz
IA.bazz
IA.bazz
IA.bazz
IA.bazz So overloading works for the extension receiver as expected, in terms of weird overload semantics of Java (which Kotlin adopted), since overloading in the same class (see But if I remove the overloaded parameter which leads to method override the static dispatch of extension functions again leads to different behavior (than with a dispatch keyword): open class IA {
// ...
open fun bar2(): IA {
println("IA.bar2")
return this
}
}
open class IB: IA() {
// ...
override fun bar2(): IB {
println("IB.bar2")
return this
}
}
open class IC: IB() {
// ...
override fun bar2(): IC {
println("IC.bar2")
return this
}
}
fun IA.bazzz(): IA {
println("IA.bazzz")
return this
}
fun IB.bazzz(): IB {
println("IB.bazzz")
return this
}
fun IC.bazzz(): IC {
println("IC.bazzz")
return this
}
fun main(args: Array<String>) {
a.bar2()
b.bar2()
c.bar2()
println()
println()
a.bazzz()
b.bazzz()
c.bazzz()
} As it results in: IA.bar2
IB.bar2
IC.bar2
IA.bazzz
IA.bazzz
IA.bazzz |
…g-extension-methods.md Adapted the proposal to the hints from @dnpetrov
@dnpetrov It now just propose to allow overriding extension functions analogous to overriding member functions. This feature is completely additive as the current behavior stays the same although I think that it is inconsistent with member functions as overloading a extension function with a function of the same signature is allowed (and statically dispatched) but not on member functions. It would be more consistent to allow this on member functions, too: open class A {
fun foo() {
println("A")
}
}
class B: A {
// should be allowed and overload A.foo since A.foo is not `open`.
// If A.foo would have been `open`, this would be denied and the
// keyword `override` is required (and then it behaves like a normal
// overriden method).
fun foo() {
println("B")
}
}
val b: A = B()
b.foo() // prints "A"
val b2 = B()
b2.foo() // prints "B" Should I add this (above) to the proposal? I address your hint regarding "special handling of overloading" in section "Outlook". |
Added description for scopes and realization. The description of a realiziation of calls to `super.foo` comes soon.
@dnpetrov: I added a description regarding the scope (as you asked me) and a section on realization in a translational fashion presenting a possible implementation (what the compiler would output) in pseudo kotlin |
A description how |
Added section "Interplay with type parameters". Adapted examples so that inheritance structure is in all examples the same.
Added kotlin-code to the realization example
Added missing `open` and `override` keywords
Description of possible realization of `super`-function
OK, I added how |
Added section Further discussion.
I added this:
...to section "Further Discussion". |
"Module" in Kotlin is a compilation unit as defined by the build system. It is not "package" (which corresponds to package in JVM). It's not clear to me how "overriding extension" members (functions and properties) should be located. In the multiple modules example (M1, M2, and M3) M2 and M3 should see different definitions on 'foo' in run-time. How this would be achieved? |
OK I will change that.
There are only the two possibilities "package-level" and "class-level", right? Since "class-level" has an implicit receiver I think it gets overly complex allowing "overriding extensions" there (if it is necessary, we can add it later on since it would be additive).
When I read the Kotlin documentation correctly, currently the scope of extension methods is either "class-level" or "package-level" and there is no "module-level" scope for them. So, this should for sure not be the case for "overriding extensions", too. I.e., you have to import an extension outside of its package (since "class-level" is not allowed, see above). But I think I described this in detail in the current version of the proposal, didn't I? What do you miss in this regard?
I will add a section for this.
The // M3 (without override)
import m1.*
import m2.*
open class E: C
val l = arrayOf(A(), B(), C(), D(), E())
// prints "A\nAB\nABC\nAD\nABC"
l.forEach { it.foo(); println() } Leading to this pseudo kotlin implementation: // M3 (without override)
import m1.*
import m2.*
open class E: C
// no need to implement dispatch method `A.foo` because it can be imported "as is".
val l = arrayOf(A(), B(), C(), D(), E())
// prints "A\nAB\nABC\nAD\nABC"
l.forEach { it.foo(); println() } If, however, a new override is added the static extension method foo (for dispatching) is not imported, but generated from scratch for the new module (e.g. M3). E.g. if we add a new class E in M3 with override // M3 (with override)
import m1.*
import m2.*
open class E: C
override fun E.foo() {
super.foo()
print("E")
}
val l = arrayOf(A(), B(), C(), D(), E())
// prints "A\nAB\nABC\nAD\nABCE"
l.forEach { it.foo(); println() } Pseudo kotlin: // M3 (with override)
import m1.*
import m2._foo
// **not** import.m2.foo !!!
open class E: C
// this is generated completely new since `A.foo` is **not** imported
fun A.foo() {
when(this) {
is E -> _foo(e = this, superFunction = {e: E -> _foo(c = e, superFunction = { c: C -> _foo(b = c, superFunction = { b: B -> _foo(a = b) }) })})
is D -> _foo(d = this, superFunction = { d: D -> _foo(a = d) })
is C -> _foo(c = this, superFunction = { c: C -> _foo(b = c, superFunction = { b: B -> _foo(a = b) }) })
is B -> _foo(b = this, superFunction = { b: B -> _foo(a = b) })
is A -> _foo(a = this)
}
}
fun _foo(e: E, superFunction: (E) -> Unit) {
superFunction(e)
print("E")
}
val l = arrayOf(A(), B(), C(), D(), E())
// prints "A\nAB\nABC\nAD\nABCE"
l.forEach { it.foo(); println() } So, there is no need for a different definition of |
If you have two different compilation units that you import in a third one, which have different implementations of |
I see one issue, do you mean this?: If I change M1 and M2 I have to recompile M3, in order to update my local Then I will change the section name to "Possible Realization" and add a "Alternative Realization" section. |
Added a paragraph on "local method override"
Added section "Alternative Realization" as well as some minor refinements
Refined usage of notion "compilation unit", "module", and "package"
I updated the proposal. It now describes an alternative realization and the issues with the realization in the proposal (which is used for ease of presentation). I added a paragraph to describe local method override as well as a corresponding question to the "Open Questions" section. Further on, I refined the usage of the notions "compilation unit", "module" (both describe the same), and package (not to be confused with the two former notions). |
Fixed some mistakes in section "Outlook"
local link test
Added internal links between sections
minor fix
More precise description of inlining.
@dnpetrov are there any open concerns? |
Sure, there are some, sorry for a long delay. I need some free cycles to explain my technical concerns in detail. |
Ok. Not a problem :) |
So far, I find the following issues with this proposal (or maybe with the approach in general):
|
Regarding your 2nd point: I already consider this case in the current version of the proposal 0228a9d . The shown "implementation" is just for presentation purposes (to give an idea of the semantics). Of course you need a virtual table approach (as I mentioned there), so that every dispatch function (in each compilation unit) does a lookup on the same vtable (which might, e.g., be filled in a static code block). I wrote the same case in my proposal as you mentioned (see Alternative Realization). And I think this is an answer for your 1st concern, too. If you have a dynamic vtable, which is filled during class/package load (e.g. via static code blocks I am not totally aware how you currently handle this in Kotlin), this vtable can contain the scope information too (I wrote an example in the proposal), which has just to be "looked up" in the dispatch function. The compiler can generate the needed dispatch functions, which are called statically. Inside it asks the vtable taking into account the scope. This could even be optimized, using/generating different dispatch functions per "call-site-scope", which need only very tailored vtable accesses (taking into account only the imports and stuff of the current call-site). These dispatch functions, should even be inlineable (or be fully unnecessary, if the corresponding lookups are generated directly to the call-site). |
On a lower level calls to virtual functions (as all non-final methods on Objects are in Java) are realized via a vtable lookup followed by a static call to the found method with the instance as implicit first parameter. Of course the lookup is more complex. Further on there might be competing extension functions on the classpath and being in scope, but like class loading it could be resolved via class path ordering (vtable entries would be "first come first serve"). There could even be runtime hints via stderr (like Java saying you are using max permgen space although this option is not available anymore). |
In Xtend they have explicit extension imports but implicit extension method definition. In Kotlin it is the other way around. The "Xtend way" you can use my "possible realization" (and they do it this way) Because you import the extension methods from the class directly and not from the package. Therefore you cannot have another compilation unit that interferes, as the class loader does not load the same class from different compilation units (it loads the class that is higher on the class path). Having two compilation units with classes in the same package is something that is (or at least should be) discouraged at all (and therefore Java security mechanism prevents this for signed jars IIRC). But you cannot be sure... so a |
Ok, so, essentially this boils down to a dispatch table built at execution time. This requires detailed high-level rules expressed in terms of class loading (NB there's no such thing as "package initialization") and class/type identity/equivalence. However, since right now we are more or less sure what the "cost" of this overall solution is likely to be, I'd suggest taking a pause as we consider some other approaches to the "expression problem" in Kotlin. |
Yes.
IIRC packages are realized as classes under the hood, aren't they? So there is a way to add a static initializer there. Or you can just put/generate a hidden class for such packages, which has the static initializer then.
OK, can you point me to that discussions? I would really appreciate, if I could see what these considerations are about :). Thanks in advance. |
Sure, I'll provide the corresponding links as something becomes available. Regarding packages in Kotlin - see http://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#package-level-functions. So far, in Kotlin we tried to keep the language abstractions transparent to Java and JVM, and designing new language features is always a search for a reasonable compromise that would both provide expressive power and keep your JVM intuition in place.
Note that you can emulate "Xtend way" (dispatching functions are (virtual) methods in classes) in Kotlin by using implicit receivers. You'll still have to write dispatch methods manually. Still, you'll be using available dispatch mechanism for extension providers instead of reimplementing it. The alternative approaches we consider now are basically different ways to pack that dispatching in platform classes, e.g., type classes with "table-passing implementation" (that's basically extension provider as an implicit parameter in Xtend terms). |
So there you have a class where you could put the initializer code.
Creating "table classes" automatically mimicking the original inheritance relations is a clever idea, especially because the JIT compiler will optimize this. I really do like the idea, but it makes it not as easy to add implementations dynamically from different compilation units (exactly the place where the "naive implementation" from my proposal fails) and having scopes. But if you use "table interfaces" instead you can hide (and therefore replace) the implementations and you can have very dedicated implementations for a given scope by "storing" a specific instance of the correct implementation in your scope at runtime. If this helps, I can add a respective section to this proposal (or somewhere else) explaining some details. |
@dnpetrov is there any news on your alternative approach? I have another case where the possibility to have (multiple) dynamic dispatch (in particular double-dispatch for extension functions) would be very handy: writing DSLs/typesafe builders. If you try to reuse a DSL or builder for different outputs, you would not put the rendering code into the meta model classes of your domain, but in extension functions, so that you can change the implementation context specific (and have SoC). I am even thinking of realizing the feature via |
A new proposal sprung from this proposal. This is a first sketch, which I hope to fill with fruitful discussions.