Conversation
from disucssions in https://discuss.kotlinlang.org/t/compound-extension/10722 Added another syntax variation using pseudo-keywords discussed here: 0a9c368
| A compound extension is declared using dotted list of types. For example, | ||
|
|
||
| ```kotlin | ||
| operator fun Body.String.unaryPlus(s: String) = escape(s) |
There was a problem hiding this comment.
This can't work. What does Map.Entry.unaryPlus apply to? Map and Entry or Map.Entry?
There was a problem hiding this comment.
I think this is discussed later on in the syntax part of the proposal. As far as I can tell, this example uses the current syntax as an introduction to the topic. This syntax clearly does not work, neither with namespaces (as pointed out below) not nested classes.
The question is which of the other possible syntax variants should be used primarily. I personally prefer parenthesis.
There was a problem hiding this comment.
It seems like square brackets give the best readability and parseability. We should remember that we need the syntax for function types. [].()-> reads better than ().()->. I think that alternative syntax solutions should be added to proposal.
There was a problem hiding this comment.
It works in either case as addressed below.
In the where there is an existing meaning then such as Map.Entry it refers to Map.Entry. If Map doesn't have a nested type named Entry it refers to the Entry in scope. If you have an Entry in scope you wish to extend, it collides with Entry but a alias can be introduced to allow you to disambiguate.
There was a problem hiding this comment.
@chuckjaz What about using the + character in place of the possibly ambiguous .?
The plus symbol on types at receiver declaration is currently not valid Kotlin, so it wouldn't break any code, and I think it'd be more clear than a dot.
There was a problem hiding this comment.
Example:
operator fun Body+String.unaryPlus(s: String) = escape(s)There was a problem hiding this comment.
Can you create a pull request to my branch to incorporate this as an alternative syntax?
There was a problem hiding this comment.
What about using the
+character in place of the possibly ambiguous.?
The plus symbol on types at receiver declaration is currently not valid Kotlin, so it wouldn't break any code, and I think it'd be more clear than a dot.
It is better, than dot, but readability is still worse than brackets. You need to remember, that the same notation for functional types. And there should be place for parameters (and parameters in theory could also have multi-receivers).
|
I do not really like the title. Event |
|
As a motivation for this proposal (not exactly current version but the one we got after discussion), I wrote an article about context-oriented approach in kotlin program design. Additional motivation could be found in kmath documentation where I actually use this approach. Currently I kind of really need it for performance optimization. I have contexts which define operations on some objects (for example nd-structures). Now I want to some functions like class NDField<T>{
...
fun NDStruncture<T>.plus(arg: T): NDStructure<T>
}This operation could benefit a lot from being moved to extension. It allows not to bloat the fun <T> [NDField<T>, NDStruncture<T>].plus(arg: T): NDStructure<T>
fun [NDField<Double>, NDStruncture<Double>].plus(arg: Double): NDStructure<Double>Being dispatched statically, those functions could use performance optimization like inlining. Context-oriented paradigm could work solely on such extensions since the method resolution is done based not on runtime type (runtime type could be used, but it is could be avoided), but on scope type. In fact, one could use empty marker scopes and dispatch all functions statically which should also benefit the performance. |
|
This seems similar in character to the |
|
As for the name, I am open to other names but "compound receiver" doesn't seem right to me as the receiver is not compound. "multiple receiver extension" is better as it is more directly what is happening. |
|
@chuckjaz Thank you for proposal! I want to highlight some not covered/problematic places. SyntaxAs @JakeWharton mentioned before, syntax Another problem here is that sometimes you want to use fully-qualified name for type i.e. Resolution rulesConsider the following example: class A {
fun B.C.foo() {}
}
class B
class C
fun A.B.foo() {}
fun test() {
with(A()) {
with(B()) {
with(C()) {
foo()
}
}
}
}This example illustrates 2 different problems:
Receivers handlingDisambiguation via Related not covered theme -- possibility to explicitly pass all implicit receivers on call site: class A
class B
fun A.B.foo() {}
fun test(a: A, b: B) {
(a, b).foo() // one possible synthaxs for that
} |
|
The alternative syntax in discussion (round or square brackets) covers all syntactic problems. @chuckjaz could you move it from discussion to the proposal as alternative? I really do not like this dot solution. As for member vs extension problem. As @orangy mentioned, we should try to keep the same behavior as was before for member extensions. I do not think that multiple receivers would change that. The member always win rule is still in place. |
|
Sometimes extensions win over members: So particular rules should be clarified. |
|
Are those rules explicitly written somewhere? It would be easier just to look at them and see what should be changed. |
|
Yes, you can find it here: https://github.com/JetBrains/kotlin/blob/master/spec-docs/NameResolution.adoc |
|
I am moving two my latest posts form discussion here with little remarks Next iteration of resolution proposalHere I will try to combine best parts of all current proposal and account for backward compatibility. The resolution could be done in following steps:
The normalization step is probably could be avoided in this scheme. The results of this procedure are the decision about binding and binding map This resolution is done via declared receiver types Compatibility check
|
Some additional thoughtsI was thinking a lot about this context-based resolution idea and I think that it is possible to go a little further (not immediately, mind you, it is the idea for the future). I am leaving this global context File level context
This mechanism is in fact currently used in KEEP-75 for script implicit receivers. But of course, for code, it should be explicit and only singleton objects will do. Extension classes and interfacesWe consider a class or interface to be top-level non-empty context. It seems to be not so hard to add a set of external contexts to the class context. It could look like Note: There will be some problem with class type parameter syntax here since unlike functions, type parameters in classes are declared after class name, but probably it could be solved. This second proposal is in fact much more flexible than first one since we can use non-singleton contexts and define them explicitly on class creation. Also, both ideas probably could cover most of type-classes use-cases. |
My intent here is not to change the rules of resolution but to add a meaning to a case that is currently an error. The proposal only kicks in, so to speak, when Kotlin would report an error.
They can by using a type alias or rename on import. This is the same case you run into if you are trying to use the type
The first found is scope wins. In this case since the In other words, the lookup should behave consistently with removing the compound receiver and being left only the simple receiver. For example, because class A {
fun C.foo() {}
}
class B
class C
fun B.foo() {}
fun test() {
with(A()) {
with(B()) {
with(C()) {
foo()
}
}
}
}will result in
The current lookup rules for extensions are unmodified. A compound receiver function
I will add that as alternative. My intent was not to add a new type label but to have the type name imply a label.
This seems to be a related but separate proposal as it would apply to extensions in generate, not just compound extensions. |
The proposal defines, given a scope, such as the scope for the body of the |
|
@altavir Thanks for providing a central place to archive discussion about this proposal. I addressed the comments attached here https://discuss.kotlinlang.org/t/compound-extension/10722/31 and modified the proposal to cover the issues surfaced in the discussion. |
|
In the section "Matching rules": "Report ambiguous calls If multiple valid candidates are possible the call is ambiguous and an error is reported", how to disambiguate and so fix the error? I wish to use a compound extension to add two integers, should I write: fun Int.Int.sum() = this@Int + this@IntThe section "Disambiguation of this" does not help for my task. Using the above function, what is val x =
with(2) {
with(3) {
with(5) {
sum()
}
}
}Is it an ambiguous call? |
|
@fvasco This is not ambiguous, it is just obscure. You can use a type alias to introduce a name to refer to the first receiver. typealias MyInt = Int
fun MyInt.Int.sum() = this@MyInt + this@IntThe value of |
|
@fvasco I think this one should be prohibited. I mean compile time error. In my variant the function will not pass the checking stage. |
|
@altavir I agree with you, this is a missing point in the documentation. If we consider the Moreover in my opinion type alias is a bad solution, Kotlin already supports label so I consider a better approach: fun A@Int.B@Int.sum() =this@A + this@B |
|
I also would like to remind everyone, that we want the feature to be gradually integrated in the language. That means restrict first, permit later approach. We need to restrict anything that could pose a problem in first implementation and think about relaxing the restriction. And again, I do not like the dot syntax. It is confusing. It supposes that we have a fixed order, but we agreed that order of receivers should not matter. Also it will be really difficult to work with function types (you should also remember that types could have parameter). By the way, if we are talking about parameters, we need to understand if two types with the same base and different parameter are allowed. I think that it should be restricted as well meaning that |
The Currently the simple name of the type is allowable as a label such as in the case of referring to the outer class in a nested class. class A {
var foo = 1
inner class B {
var foo = 2
fun addFoos() = this@A.foo + foo
}
}My proposal uses something similar to disambiguate the receivers just as the above.
The order of the receiver parameters change. |
|
Should this proposal also allow for multiple receivers in lambda types? |
|
It was somewhere in discussion if not in the proposal itself. Yes, of course it does not make sense without multiple receiver function types. |
|
Looking at this, I am not sure that it can work without a syntax for precise multi-receiver invocation. Currently you can do precise invocations as there are only 2 receivers and you can use implicit receiver vs explicit receiver. When multiple receivers (>2) are present that no longer works. You probably also want to have both syntaxes be consistent. |
|
I am not sure how >2 case is different from case of double receiver. Of course, I am not talking about dot notation. We can easily have something like |
|
@altavir At least in the current situation there are three different ways to invoke a multi-receiver function:
These three have different resolution/priority rules that can be used, for example when In the case of compound extensions with 2 extension types the existing rules could/should be reused as there should not be a difference in static resolution of compound extension vs member extension. However in the case of more than 2 extension types it is no longer possible to reuse these rules and a new (more robust) is needed for those cases. In other words if I have an extension function that takes multiple types ( I agree that in most cases the issue doesn't come up, but in case it does, it must be handled correctly. Currently The alternative, somehow depending on the order in which receiver candidates were introduced into the context is something that from my perspective too opaque. I'd much prefer the compiler to throw an error and make me specify in a clearly legible way what the intended receivers are, and what their order is. And casting variables to remove ambiguity is not really a desirable solution to me (although would be something that developers could not be stopped from doing). |
|
@HKhademian is giving an example from Android development. In the Android API there is a function that converts device independent pixel values to physical device's pixel values. This function is an instance member of a class named Context (ambiguous in the context of this discussion). It is commonly requested by Android developers to use "10.dip". This would require 2 receivers: the Context object that can do the conversion and the integer value itself. |
|
I have not developed for Android for a long time, but still this requirement seems rather artificial to me. That is what I would have done: class ScreenUnits(val dpSize: Int){
val Int.dip: Int get() = this*dpSize
}And then perform all screen operations in the context of this class. From architectural point of view, it would also be good to have all screen rendering features in a single class, not only density. With multiple receivers it is also possible to do following: val [Context,Int].dip: Int get() = this@Context.density * this@Int |
|
Your first example would still require boilerplate that he wanted to get rid of. Your last example is exactly what he wants to achieve. He just uses a different syntax for it. I am not familiar with that syntax, but apparently it comes from C#. I like your syntax more. |
|
The two examples are essentially the same. The difference is that you can use second one with existing classes, while first one requires some changes to the API. For example adding the function to Context. |
|
So you mean the requirement is artificial because the Android API is poorly designed? I am not sure whether that qualifies as artificial-ness, but getting off-topic... |
|
A somewhat related keep: It proposes to add "extension values" to classes (called "companion values" in the proposal). Such values would implicitly be provided as receivers to all member functions of the class. |
|
I experimented a bit in #106 to implement companion vals in the compiler and i added a test there how to realize the compound receiver use case with it. It doesnt use the syntax using all receivers as prefix and i indeed think that using a single extension receiver is better, combined with a companion, or extension parameter, because semantically one wants to invoke a function/access a property on a single thing, the rest is just context. My test case also shows a slightly different way, modeling the compound receiver as an explicit class, having only its properties companions. This enables the nice dsl usage for multiple receivers, while it eliminates implicit parameter deduction from the context, because the instance must still be provided explicitly. |
|
I don't think that #106 can replace compound receivers. Neither vice versa. They might have intersections but are mostly orthogonal features. One of my main use cases for compound receivers would be top level functions. I cannot use #106 for that. In fact in my use case compound receivers would work very very well together with #106 . Right now in my project we are solving the use case with some boilerplate, and we look forward for both of these keeps. If there is interest, I can provide the use case in more detail. |
|
Did you take a look at my prototype implementation? There is a test case implemented with top level function, although i extended the implementation's scope compared to the original proposal. |
|
Does your prototype allow providing extension values as the argument of top level functions? In that case it would be usable in my use case, however it would mean boilerplate at the call site (where the top level function is called). |
|
I invested the time to prepare a simplified version of my use case in which applying compound extensions as well as companion / extension values ( #106) would reduce some boilerplate. More importantly, it would allow for better encapsulation and also make the code more flexible. Neither of both proposals would be sufficient for its own to achieve that in this example. This is plain old Kotlin code, that does not make use of neither proposal. Applying the proposals is done in the next code snippet after this one. The magic happens inside handleRequest() as well as homeLink() functions: interface I18nMessages {
fun getMessage(key: String)
}
interface I18nContext {
val messages: I18nMessages
fun getMessage(key: String) = messages.getMessage(key)
}
interface UrlBuilder {
fun getUrl(key: String)
}
interface UrlContext {
val urls: UrlBuilder
fun getUrl(key: String) = urls.getUrl(key)
}
class FooPageController(
override val messages: I18nMessages,
override val urls: UrlBuilder
) : I18nContext, UrlContext {
fun handleRequest() = FooModel(
windowTitle = getMessage("foo.windowTitle"),
fooHeader = getMessage("foo.headerLabel"),
homeLink = homeLink()
)
}
// a global function that is usable in every controller:
fun <T> T.homeLink() where T : I18nContext, T : UrlContext = Link(
url = getUrl("urls.homePage"),
label = getMessage("label.link.homePage")
)
data class FooModel(
val windowTitle: String,
val fooHeader: String,
val homeLink: Link
)
data class Link(val url: String, val label: String)Update: I now refactored the example to take advantage of compound extension function as well as companion values (#106). The syntax seen in following code adheres to the proposed syntax in these 2 keeps at the time of this writing: interface I18nMessages {
fun getMessage(key: String)
}
interface UrlBuilder {
fun getUrl(key: String)
}
class FooPageController(
private companion val messages: I18nMessages,
private companion val urls: UrlBuilder
) {
fun handleRequest() = FooModel(
windowTitle = getMessage("foo.windowTitle"),
fooHeader = getMessage("foo.headerLabel"),
homeLink = homeLink()
)
}
// a global function that is usable in every controller:
fun [I18nMessages, UrlBuilder].homeLink() = Link(
url = getUrl("urls.homePage"),
label = getMessage("label.link.homePage")
)
data class FooModel(
val windowTitle: String,
val fooHeader: String,
val homeLink: Link
)
data class Link(val url: String, val label: String)In my opinion this looks great and feels very much like Kotlin. |
|
Thanks for the example! When interfaces only are involved, indeed everything is more simple. There wouldn't be a need for the exports feature from companion vals at all because one can use standard interface delegation. But i think this proposal is about cases where you already have two objects and want to work with types that are not necessarily interfaces. |
|
This is a simplified version of real world example, in which we indeed have the same kind of interfaces. The *Context interfaces exist only for the purpose shown in this example. They are interfaces because this allows to inherit code from multiple interfaces. The I18nMessages and UrlBuilder types are interfaces because of the usual clean code considerations. Of course there are concrete implementations for them, but those concrete implementations are not known at compile time. |
|
I think a good language for today needs is a language that behave like all other todays usual tasks. As our daily life experiences, when we are in a context, like a room, we can do some actions on inner objects (like phone charger) that only possible if we be there, like connect it to power outlet. And we don't redefine charger when we change our context, we just define some common actions on some special objects according to its context. I'm an Android Developer, I use Anko to build my ui layouts. when I use anko, I face some problems as simple as in Anko we have child for every common there is no problem, until you define your own ui, or want to extends those But if we implement this multi context receiver parameter in extension functions or even package level functions, I think we have better code, less code boilerplates, and better libraries even out of android ecosystem. And as I mentioned before, I like these multi receiver context parameters to have their own names, because, because it can't solve some rare problems on same type receivers like And for last words, I think #106 is a good idea to join kotlin community (but first need many polishes), but it's not a solution for all our pains (boilerplate codes, ...). Think you need to mention verticalLayout {
private companion val contextHelper1 = AnkoLinearLayoutContextHelper // <- it's an object
// why I need to define a variable when I don't use it directly, I may discuss it on #106
frameLayout {
private companion val contextHelper2 = AnkoFrameLayoutContextHelper // <- it's an object too
// what the point if we have code boilerplates again, I know, discuss in #106
....
}
}I recommend something like these: fun Context.dip(this /* or with */ size: Number) = ...
fun ViewGroup.lparams(this view: View) = ...
fun FrameLayout.lparams(with view: View) = ...
// thinks about these beauties (There is no default context definition)
fun avatarize(this toolbar: ToolBar, this avatar: AvatarView = CircularAvatar()) = ...
// --- and use it like:
toolbar {
avatarize()
squareAvatar.avatarize()
}
// or
squareAvatar {
toolbar.avatarize()
}
// try infix
infix fun hit(this player1: Player, this player2: Player) = ...
// --- and use it like:
mainPlayer.apply {
hit enemy
}
// maybe triple or more receivers even vararg, of course it rare case, but who we are to ban it
fun cubeFromPoints(with vararg points: Point) = ... |
|
I read my codes above and I think, if multi receiver context parameter extension functions (what a like title for a tiny feature 😃 ) with no default receiver as new version (but not sure): toolbar {
avatarize()
avatarize(squareAvatar)
avatarize(newToolbar, squareAvatar)
squareAvatar.avatarize()
squareAvatar.avatarize(otherToolbar)
}
squareAvatar {
toolbar.avatarize()
toolbar.avatarize(someOtherAvatar)
avatarize(toolbar)
avatarize(toolbar, someOtherAvatar)
someOtherAvatar.avatarize(toolbar)
someOtherAvatar.avatarize()
}I think we can have multi stage to implement #176 (multi receiver context parameter extension functions). |
|
I updated my example to take advantage of compound extension as well as companion values: |
|
I believe that this keep together with "companion values" ( #106) could support many of the use cases that are given to motivate type classes ( #87). I added an example that illustrates this in that keep: |
|
One example where this could be useful is extending certain parts of Gradle builds. For example: // Suppose we want to abstract adding multiple dependencies of the same "kind"
// whose version is defined in the project properties, or otherwise derived from
// the `Project` instance
val groupVersion: String by project.extra
dependencies {
listOf("name1", "name2", "name3").forEach {
implementation("group", it, groupVersion)
}
}
// It'd be nice to be able to abstract the above like this:
dependencies {
configurations.implementation(groupDependencies)
// while also allowing other configurations to be used:
configurations.testRuntimeOnly(groupDependencies)
}
// Unfortunately, at this moment it is not possible to do this nicely without
// multi-receiver extensions, because constructing and adding a dependency
// would require three objects: `Project`, `DependencyHandler` and `Configuration`:
class Dependencies(scope: DependencyHandlerScope) {
fun configure(project: Project, scope: DependencyHandler, configuration: Configuration)
}
val groupDependencies: Dependencies = object : Dependencies {
override fun configure(project: Project, scope: DependencyHandler, configuration: Configuration) {
val groupVersion: String by project.extra
scope {
listOf("name1", "name2", "name3").forEach {
configuration("group", it, groupVersion)
}
}
}
}
fun Project.DependencyHandler.Configuration.invoke(dependencies: Dependencies) {
dependencies.configure(this@Project, this@DependencyHandler, this@Configuration)
}There are certainly other ways to do this, but none (as far as I can see) would allow the "native" syntax like |
|
Oleg Yukhnevich just presented an example which is currently working, but would be broken on a non-ordered multi-receiver resolution: object A {
fun B.ext()= println("AB")
}
object B{
fun A.ext() = println("BA")
}
fun main(){
with(A) {
B.ext()
}
with(B) {
A.ext()
}
}There are two solutions to that problem:
I prefer the first variant. |
|
|
||
| ### Matching rules | ||
|
|
||
| 1. **Create an ordered list of implicit receivers** in scope with types |
There was a problem hiding this comment.
I would like to see support for explicit receivers for multi-receiver extensions. Existing extensions can be called with an explicit or implicit receiver, and I think there are cases where explicit receivers could be important for multi-receiver extensions.
As noted below, receivers may be "explicitly" provided with the following syntax, but it extremely verbose and not obvious:
with (a) {
with (b) {
with (c) {
doSomething()
}
}
}
There was a problem hiding this comment.
What about:
with (a,b,c) { doSomething() }
Where the receivers match the order of the receivers of doSomething
| operator fun Body.String.unaryMinus() = underline { +this@unaryMinus } | ||
| ``` | ||
|
|
||
| ## Proposal |
There was a problem hiding this comment.
This proposal does not address limitations regarding two receiver types being related. Would it be possible to define an extension with two receivers of the same type, and if so, how would they be disambiguated?
There was a problem hiding this comment.
This question is answered in the discussion starting at #176 (comment)
|
@DavidDTA There was a lot of discussions about the proposal and there are actually several different variants of it in the discussion. Sadly, the initial text is not updated anymore. And KEEP is now officially not the place for such discussion. It should continue at https://youtrack.jetbrains.com/issue/KT-42435. |
|
Thanks a lot for a fruitful discussion on this issue. The use-case of writing this kind of "compound extensions" in Kotlin is important. However, this specific design proposal does not meet the basic requirements that were identified for this kind of feature in Kotlin. There is no specific design yet to address the use-cases that are covered here, but there is a YouTrack issue that tracks this problem and has some basic requirements that a design shall meet that were identified thus far: https://youtrack.jetbrains.com/issue/KT-10468 Please, vote for it, comment on your specific use-cases, and continue discussions there 👍 With this, I'm closing this KEEP issue. |
A compound extension proposal as discussed here: https://discuss.kotlinlang.org/t/compound-extension/10722