Skip to content

Compound extension#176

Closed
chuckjaz wants to merge 3 commits intoKotlin:masterfrom
chuckjaz:compound-extension
Closed

Compound extension#176
chuckjaz wants to merge 3 commits intoKotlin:masterfrom
chuckjaz:compound-extension

Conversation

@chuckjaz
Copy link
Copy Markdown

@chuckjaz chuckjaz commented Jan 4, 2019

A compound extension proposal as discussed here: https://discuss.kotlinlang.org/t/compound-extension/10722

A compound extension is declared using dotted list of types. For example,

```kotlin
operator fun Body.String.unaryPlus(s: String) = escape(s)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can't work. What does Map.Entry.unaryPlus apply to? Map and Entry or Map.Entry?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example:

operator fun Body+String.unaryPlus(s: String) = escape(s)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you create a pull request to my branch to incorporate this as an alternative syntax?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@altavir
Copy link
Copy Markdown

altavir commented Jan 5, 2019

I do not really like the title. Event compound receiver would be be better. But commonly used name for this feature is multiple receiver extension.

@altavir
Copy link
Copy Markdown

altavir commented Jan 5, 2019

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 plus to be defined in the context:

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 NDField class and avoid complicated type constructs in inheritance. What is more important it allows to create an optimized implementations for specific types without complicated inheritance structures of field itself:

  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.

@chuckjaz
Copy link
Copy Markdown
Author

chuckjaz commented Jan 7, 2019

This seems similar in character to the escape html DSL example or am I missing something?

@chuckjaz
Copy link
Copy Markdown
Author

chuckjaz commented Jan 7, 2019

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.

@erokhins
Copy link
Copy Markdown
Contributor

erokhins commented Feb 4, 2019

@chuckjaz Thank you for proposal!

I want to highlight some not covered/problematic places.

Syntax

As @JakeWharton mentioned before, syntax fun A.B.foo() can has 2 different meaning: function with receiver A.B (where B is nested class of A) or function with two receivers: A and B.
Ambiguity can be resolved as you proposed (nested class wins), but user cannot force compiler use another meaning: two receivers.

Another problem here is that sometimes you want to use fully-qualified name for type i.e. fun a.b.Foo.x.y.Bar.bas() and in such cases resolution rules will be even worse (especially in package a we have class b and so on).

Resolution rules

Consider 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:

  1. what we should do if our candidates have different amoung of receivers? (We can consider that member extension function foo in class A have 3 receivers: A, B, C)
  2. should be members wins over extensions?

Receivers handling

Disambiguation via this@A where A is type name introduce entirely new meaning for labels -- label by type name. I would try to avoid that. Instead of that maybe we can mark receiver explicitly if necessary.

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
}

@erokhins erokhins self-assigned this Feb 4, 2019
@altavir
Copy link
Copy Markdown

altavir commented Feb 4, 2019

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.

@erokhins
Copy link
Copy Markdown
Contributor

erokhins commented Feb 4, 2019

Sometimes extensions win over members:

class A {
    fun foo() {}
}
class B

fun B.foo() {}

fun test() {
    with(A()) {
        with(B()) {
            foo() // resolved to B.foo
        }
    }
}

So particular rules should be clarified.

@altavir
Copy link
Copy Markdown

altavir commented Feb 4, 2019

Are those rules explicitly written somewhere? It would be easier just to look at them and see what should be changed.

@erokhins
Copy link
Copy Markdown
Contributor

erokhins commented Feb 4, 2019

@altavir
Copy link
Copy Markdown

altavir commented Feb 4, 2019

I am moving two my latest posts form discussion here with little remarks

Next iteration of resolution proposal

Here I will try to combine best parts of all current proposal and account for backward compatibility. The resolution could be done in following steps:

  1. Create a list of actual contexts G, A, B, C, ... where G is a global context. Top level non-extension functions have only G as context. Class members have that class as context. Extension function have their receivers added to context where they are defined. Running a function with receiver adds that receiver to the list of contexts where this function is defined.Types in the list could be duplicating. We will call those actual types Cy where y is the index
  2. Checking the definition of the function. Function receiver list is written in form of [R1, R2, R3]. Duplicate types or ambiguities throw compile error. Types could have parameters defined outside type list like fun <T> [T, Operation<T>].doSomething(). We will call receiver types Rx where x is the index.
  3. Binding of extension function. Each of types R in receiver set is matched against the elements of context list from right to left, binding it to first match and thus creating a map Rx to Cy. If there is a bound pair for each of Rx then function is considered resolved and bound to context. Multiple R could be bound to the same context C, so it is possible to have just one context for multiple receiver types if it matches them both.

The normalization step is probably could be avoided in this scheme. The results of this procedure are the decision about binding and binding map Rx to Cy.

This resolution is done via declared receiver types Rx which are then substituted by actual runtime objects representing Cy. Since the binding is done to the closest receiver matching the type, this will also represent the closest receiver of matching type.

Compatibility check

  • [A].f === A.f.Extension function with single argument should work exactly like existing extension function. It seems like it does. It is resolved and bound to the closest context matching its type.

  • Match current member receivers strategy. Seems to be working the same way. It resolves to the closest context even if this context implements both receivers.

@altavir
Copy link
Copy Markdown

altavir commented Feb 4, 2019

Some additional thoughts

I 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 G everywhere in my schemes and it does not play any important role because it usually is empty. But in fact it could provide a lot of opportunities.

File level context

G is basically resolved to file which does not have any type of its own. But if there was a way to bind a context or a set of contexts to the file itself (not proposing any syntax solution, but it should be quite easy). Then everywhere in my schemes above we will just replace G with G, F1, F2. Meaning that all classes and functions in this file will have additional implicit contexts, just like type-classes. Of-course, it means that Fs could only be singletons in this case.

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 interfaces

We 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 class [T1,T2].MyClass{}. In this case the instance of this class could be created only in context bound to both T1 and T2 and all members of this class will have additional implicit contexts, meaning member of MyClass will be able to call members of T1 without additional declarations. From the implementation point of view, the instances of T1 and T2 could be class constructor implicit parameters (or a single map parameter, which is probably better), it should not violate anything, even Java compatibility (you will have to pass instances of contexts to create class instance). The interfaces could work the same way, just provide synthetic (invisible) property which holds the Map<KClass, Any> of actual receiver.

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.

@chuckjaz
Copy link
Copy Markdown
Author

chuckjaz commented Feb 4, 2019

I want to highlight some not covered/problematic places.

Syntax

As @JakeWharton mentioned before, syntax fun A.B.foo() can has 2 different meaning: function with receiver A.B (where B is nested class of A) or function with two receivers: A and B.

Another problem here is that sometimes you want to use fully-qualified name for type i.e. fun a.b.Foo.x.y.Bar.bas() and in such cases resolution rules will be even worse (especially in package a we have class b and so on).

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.

Ambiguity can be resolved as you proposed (nested class wins), but user cannot force compiler use another meaning: two receivers.

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 Bar from x.y in a package that defines its own Bar. One must be renamed or otherwise aliased.

Resolution rules

Consider 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:

  1. what we should do if our candidates have different amoung of receivers? (We can consider that member extension function foo in class A have 3 receivers: A, B, C)

The first found is scope wins. In this case since the fun B.C.foo() in class A wins. The number of receivers is defined by the function being called. In this case, it requires 3, A (as this) B and C as receivers. Since fun A.B.foo() is not found the fact it takes 2 receivers is not relevant.

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 A's fun C.foo() being called as it is in scope prior to fun B.foo(). The previous example should result in fun B.C.foo() from class A, and, identically, because fun B.foo() would receive two receivers today (A as the this parameter and B as the receiver parameter) and the presence of foo B.foo() {} doesn't affect this, the function fun B.C.foo() would receiver three and is unaffected by foo A.B.foo() in scope.

  1. should be members wins over extensions?

The current lookup rules for extensions are unmodified. A compound receiver function foo A.B.foo() should be thought of as extending the type A with a member fun B.foo(), which extends the type B with fun foo(). A direct member of B has precedence over all extensions.

Receivers handling

Disambiguation via this@A where A is type name introduce entirely new meaning for labels -- label by type name. I would try to avoid that. Instead of that maybe we can mark receiver explicitly if necessary.

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.

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
}

This seems to be a related but separate proposal as it would apply to extensions in generate, not just compound extensions.

@chuckjaz
Copy link
Copy Markdown
Author

chuckjaz commented Feb 4, 2019

Sometimes extensions win over members:

class A {
    fun foo() {}
}
class B

fun B.foo() {}

fun test() {
    with(A()) {
        with(B()) {
            foo() // resolved to B.foo
        }
    }
}

So particular rules should be clarified.

The proposal defines, given a scope, such as the scope for the body of the with (B()), how does one determine if foo() matches an extension function in scope. It does not affect how scopes are composed. The example above illustrates what happens when a member of B obscures a member of A. This proposal will have an effect on this case but only as far as this proposal affects how members are found in the scope of B.

@chuckjaz
Copy link
Copy Markdown
Author

chuckjaz commented Feb 4, 2019

@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.

@fvasco
Copy link
Copy Markdown

fvasco commented Feb 4, 2019

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@Int

The section "Disambiguation of this" does not help for my task.

Using the above function, what is x value of the code:

val x =
 with(2) {
  with(3) {
   with(5) {
    sum()
   }
  }
 }

Is it an ambiguous call?

@chuckjaz
Copy link
Copy Markdown
Author

chuckjaz commented Feb 5, 2019

@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@Int

The value of x above is a 8 as you would expect. It is not ambiguous as one and only one function would match fun MyInt.Int.sum(). The receivers are calculated as nearest where MyInt binds to the Int scope introduced by with(3) and Int binds to the receiver introduced by with(5).

@altavir
Copy link
Copy Markdown

altavir commented Feb 5, 2019

@fvasco I think this one should be prohibited. I mean compile time error. In my variant the function will not pass the checking stage.
@chuckjaz I am not sure that type alias is a good idea. I do not know how typealiases are resolved by compiler, but using them for this resolution could bring additional level of complexity. What will happen if ones switches order of with in example?

@fvasco
Copy link
Copy Markdown

fvasco commented Feb 5, 2019

@altavir I agree with you, this is a missing point in the documentation.

If we consider the this scopes as a list then no permutation should be allowed (because the position is relevant, see "Matching rules"), otherwise if we consider the this scopes as a set then no duplication should be allowed (because multiple permutation matches).

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

@altavir
Copy link
Copy Markdown

altavir commented Feb 5, 2019

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 [Box<A>, Box<B>] (or Box<A>.Box<B> in dot notation) should be prohibited as well.

@chuckjaz
Copy link
Copy Markdown
Author

chuckjaz commented Feb 5, 2019

@chuckjaz I am not sure that type alias is a good idea. I do not know how typealiases are resolved by compiler, but using them for this resolution could bring additional level of complexity.

The this scopes would exist, using a label just disambiguates them allowing you to access a possibly obscured member or the instance directly as in the example.

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.

What will happen if ones switches order of with in example?

The order of the receiver parameters change.

@BenWoodworth
Copy link
Copy Markdown

BenWoodworth commented Apr 5, 2019

Should this proposal also allow for multiple receivers in lambda types?
E.g. (T1, T2).() -> R:

inline fun <T1, T2, R> with(receiver1: T1, receiver2: T2, block: (T1, T2).() -> R): R {
    with(receiver1) {
        with (receiver2) {
            return block()
        }
    }
}

@altavir
Copy link
Copy Markdown

altavir commented Apr 5, 2019

It was somewhere in discussion if not in the proposal itself. Yes, of course it does not make sense without multiple receiver function types.
Also, we definitely will want with(receiver2, receiver2).

@pdvrieze
Copy link
Copy Markdown

pdvrieze commented Apr 8, 2019

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.

@altavir
Copy link
Copy Markdown

altavir commented Apr 8, 2019

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 val func: [A, B, C].()->Any signature and invoke it with with(b, c, a){func()} notation. I believe that common consensus is that order should not matter.

@pdvrieze
Copy link
Copy Markdown

pdvrieze commented Apr 8, 2019

@altavir At least in the current situation there are three different ways to invoke a multi-receiver function:

  • with(a) { b.foo() }
  • with(b) { a.foo() }
  • with(a) { with(b) { foo() } }

These three have different resolution/priority rules that can be used, for example when a and b are of related types, and sometimes some of these variants do or do not work - cause errors.

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 (C:B B:A and A) and is intended to have 3 receivers all of type C. how does the compiler determine the order of the receivers on invocation? And how does it even pick all three rather than just the innermost object as it satisfies the constraints.

I agree that in most cases the issue doesn't come up, but in case it does, it must be handled correctly. Currently with is a regular library function, so it could be syntactic sugar, but it would in its own code have the exact same problem as it is intended to solve. Making with a function-that-is-always-an-instruction-and-has-special-meaning is possible, but I don't like it as a solution as it also introduces a scope. I would want a way to basically call (a,b,c).foo and have that have the intended meaning (order for member extension functions would need to be specified).

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).

@fatihcoskun79
Copy link
Copy Markdown

fatihcoskun79 commented Jul 30, 2019

@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.

@altavir
Copy link
Copy Markdown

altavir commented Jul 30, 2019

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

@fatihcoskun79
Copy link
Copy Markdown

fatihcoskun79 commented Jul 30, 2019

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.

@altavir
Copy link
Copy Markdown

altavir commented Jul 30, 2019

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.

@fatihcoskun79
Copy link
Copy Markdown

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...

@fatihcoskun79
Copy link
Copy Markdown

A somewhat related keep:

#106

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.

@hannomalie
Copy link
Copy Markdown

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.

@fatihcoskun79
Copy link
Copy Markdown

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.

@hannomalie
Copy link
Copy Markdown

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.

@fatihcoskun79
Copy link
Copy Markdown

fatihcoskun79 commented Jul 30, 2019

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).

@fatihcoskun79
Copy link
Copy Markdown

fatihcoskun79 commented Jul 30, 2019

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.

@fatihcoskun79 fatihcoskun79 mentioned this pull request Jul 30, 2019
@hannomalie
Copy link
Copy Markdown

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.

@fatihcoskun79
Copy link
Copy Markdown

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.

@HKhademian
Copy link
Copy Markdown

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 dip(16) or view.lparams and when I look at how anko provides some simple actions like them, I wish, kotlin has some simple solutions as always to solve it.

in Anko we have child for every common ViewGroups. and why? to implement a simple function that create its special LayoutParam instance like _LinearLayout { fun View.lparams(...) = LinearLayout.LayoutParams(...) }, _FrameLayout { fun View.lparams(...) = FrameLayout.LayoutParams}, ...
(it's hard to write even these structural short versions, think about tens or even hundreds of classes to extends (if possible) to bring this single functionality)

there is no problem, until you define your own ui, or want to extends those ViewGroups or even use a library that extends those. in that point there is no _LinearLayout { fun View.lparam(...) } or any other helper functions.

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 fun [View, View].pack() = ...
Because I like C# (of course less than Kotlin), I think if we define them with some prefixes like with (better relation with kotlin current definition) or this (better meaning), we have more option to join this concept with other kotlin features like optional parameters, infix functions, ..

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 private companion val contextHelper = AnkoContextHelper in every layout layers like:

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) = ...

@HKhademian
Copy link
Copy Markdown

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 avatarize function I defined above,has limits on how we call them, on overall codes has cleaner schema and easier usage:

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).
First add simple definitions, then add these extra features to it.

@fatihcoskun79
Copy link
Copy Markdown

fatihcoskun79 commented Jul 30, 2019

I updated my example to take advantage of compound extension as well as companion values:
#176 (comment)

@fatihcoskun79
Copy link
Copy Markdown

fatihcoskun79 commented Jul 30, 2019

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:
#87 (comment)

@netvl
Copy link
Copy Markdown

netvl commented Jan 2, 2020

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 <configuration>(<dependency details>).

@altavir
Copy link
Copy Markdown

altavir commented Jan 24, 2020

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:

  • Just throw compile-time exception if function have an ambiguous binding as it stated in bounding rules. The given code is can't have a lot of usages since it is obviously bad.
  • Add additional rule for special treatment of first receiver in case it is a member receiver. It could require a lot of rule tweaking.

I prefer the first variant.


### Matching rules

1. **Create an ordered list of implicit receivers** in scope with types
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()
        }
    }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This question is answered in the discussion starting at #176 (comment)

@altavir
Copy link
Copy Markdown

altavir commented Dec 15, 2020

@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.

@elizarov
Copy link
Copy Markdown
Contributor

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.

@elizarov elizarov closed this Dec 16, 2020
@SPC-code SPC-code mentioned this pull request Jan 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.