Skip to content
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

Companion values #106

Closed
wants to merge 11 commits into from
Closed

Companion values #106

wants to merge 11 commits into from

Conversation

sakno
Copy link

@sakno sakno commented Apr 24, 2018

Propose syntax for implicit access to members of the value declared as companion value.

@sakno sakno mentioned this pull request May 15, 2018
protected companion val: ILogger = LoggerFactory.getLogger(A::class.qualifiedName)

fun foo(){
info("Log entry") //the same as [email protected]
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just logger.info()? To save a few characters?

Copy link
Author

Choose a reason for hiding this comment

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

companion object also helps to save few characters as well as apply and run functions from stdlib. The same story here.

Copy link
Contributor

Choose a reason for hiding this comment

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

But one big difference between this feature and apply/run/let/etc that scope functions do not require any additional language features, all of them are just library functions

Copy link
Author

Choose a reason for hiding this comment

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

Agree, but companion object is a language feature with similar behavior.

### Constructor parameter
Ability to aggregate object passed as constructor parameter:
```kotlin
class Derived(private companion val b: Base1): Base2(a), Serializable
Copy link
Contributor

Choose a reason for hiding this comment

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

Actually looks somehow related to typeclasses proposal that also tries to provide ad-hoc polymorphism.

Copy link
Author

Choose a reason for hiding this comment

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

Not sure about typeclasses. This declaration has the same behavior as declaration of companion value at class level.

class Derived(private companion val b: Base1){ }

is equal to

class Derived(b: Base1){
  private companion val: Base1 = b
}

@stangls
Copy link

stangls commented Aug 2, 2018

Maybe you should point out that public companion val is not allowed (and possibly why: diamond problem) and if and under what circumstances multiple companion vals at class-level are allowed.

@stangls
Copy link

stangls commented Aug 2, 2018

When a companion val should be accessible from outside (via name), I could write something like this, but it seems ugly to me:

class Derived(a: Int, b: Int): Base2(a), Serializable{
  private companion val internalBase1 = Base1(a + b) // needs internal name
  val base1 get() = internalBase1 // actual name of the base1
  
  fun foo() = baz() //baz() from Base1
}

fun outside() {
  Dervied(0,1).base1.baz() // accessible
  Dervied(0,1).baz() // compile time error: function baz does not exist
}

Another syntax would be to not mix the visibility of the companion object with the visibility of the field, for example

class Derived(a: Int, b: Int): Base2(a), Serializable{
  val base1 = Base1(a + b)
  private companion val base1 // makes existing base1 accessible via `this` inside this class
  
  fun foo() = baz() //baz() from Base1
}

fun outside() {
  Dervied(0,1).base1.baz() // accessible
}

@hannomalie
Copy link

hannomalie commented Jul 19, 2019

Hey guys, I find this proposal very interesting, as it mixes (the less controversial) aspects from KEEP-87 (type classes) with aspects of KEEP-176 (compound extensions). I think possibilities of companion vals are still described a bit sparsely here, but I did a prototype implementation of my vision of this feature in https://github.com/hannespernpeintner/kotlin/tree/keep-106 . Take a look at the readme and the test cases I added. From my point of view, the companion keyword could be used intuitively at any scope, that's why I enabled it at every (?) scope level (see tests at https://github.com/hannespernpeintner/kotlin/tree/keep-106/compiler/testData/codegen/box/companionval).

Some disclaimers:

  • I like KEEP-87
  • I used code from them
  • I did this for fun
  • My implementation is veeeery poor and incomplete. For example I don't know why there are no auto completion proposals in the IDE, despite the companion val call can be resolved and doesn't give errors.

@hannomalie
Copy link

Hey, it's me again. Since I wasn't able to implement above mentioned bit in the compiler itself, I did a quick try to implement an annotation processor, that generates accessors for members of companion properties of a class. Works somewhat well, maybe take a look at the repository if someone is interested.

@fatjoem
Copy link

fatjoem commented Jul 30, 2019

I like the idea but I don't like the usage of keyword "companion" for this.

The proposal adds a feature to classes that already exists for functions: For functions this feature is called "extension function".

Indeed, with this proposal every function in the class implicitly becomes an extension function, whose extension receiver is implicitly provided.

Hence I would propose to rename this proposal "extension values" and use a new "extension" (or extend) keyword:

extension val a: Context = ...

All functions in a class that has such property would implicitly become an extension function whose receiver would be implicitly set to a.

Disclaimer: The JVM language "Xtend" has a very similar feature and they also use a similar keyword for this (extend or extension).

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

I think you slightly missed the point here :). The extension function feature makes a function capable of being invoked on a receiver which happens at declaration site. Companion vals do two other things. First, they are made available as a receiver at the use site. Thus not making functions of the surrounding class extension functions, but the other way around: making extension functions of the companion val appear as if they were regular functions. The other thing is that public companion vals of a class export the val's members, so that other use sites can use them on the surrounding instance rather than on its member.

I think this is more similar to what companion objects do, hence the name. Extension providers in extend do a slightly other thing, because they indeed make extension functions out of regular functions.

@fatjoem
Copy link

fatjoem commented Jul 30, 2019

Your description does not match the proposal, which reads:

"companion keyword can be applied to val or var declarations inside of class declaration or at constructor parameter level [i.e. on a property P inside of class X]. This keyword indicates that all members of such value [P] will be accessible implicitly inside [members of] class [X]"

In other words: the functions of class X implicitly become extension functions that take P as a receiver. Therefore members of P become available inside those functions. The receiver P does not need to be provided explicitly, it is provided implicitly.

I did not give much thought to the public property case, as that one is not very well described in the proposal.

@fatjoem
Copy link

fatjoem commented Jul 30, 2019

To be a bit more explicit, this is the example of the proposal rewritten to use extension functions:

class Derived(a: Int, b: Int) {
  val base1 = Base1(a + b)
  
  fun Base1.foo() = baz() // invoked on base1

  fun test() = base1.foo()
}

This already works today, and it uses extension functions. The proposal basically defines syntactic sugar for it, by making the functions implicitly receive the receiver.

@hannomalie
Copy link

Yes, you are right, my description doesn't match the proposal, because as in #106 (comment) mentioned, i don't see a reason to artificially limit the companion keyword location, as opening up allows for use cases that are definitly demanded by people (compound receiver). I wanted to put this to discussion and added example tests for use cases.

Your example However is not correct i think. Turning sth into an extension function doesn't give us anything, because it would be now the caller's job to provide the receiver, which is clearly not the case in the original example. That's why i said the difference is use site vs declaration site. In your example, baz() is not invoked on base1, but on an arbitrary Base1 instance the user puts into scope before using Base1.foo()

@fatjoem
Copy link

fatjoem commented Jul 30, 2019

Turning sth into an extension function doesn't give us anything, because it would be now the caller's job to provide the receiver, which is clearly not the case in the original example

I repeated multiple times that the receiver is provided implicitly, removing the burden of providing it explicitly at the call site.

I am not saying that the implementation must follow this description. But conceptually this is what happens here.

I understand that you allow this feature at smaller scopes in your prototype. That's cool. But my interpretation of the feature still holds conceptually. Just at smaller scopes.

@hannomalie
Copy link

Than i must admit that i don't get it, maybe it's my language barrier, I'm sorry :) Could you take your last example and explain how the receiver of type Base1 should be brought into scope, other than the user does sth like with(...){}??

I find this very confusing.. i think a better comparison would be with Not making things extension functions, because functions could already be extension functions, but with inserting with Statements everywhere in the class surrounding the companion... This would be equivalent:

class Derived(a: Int, b: Int) {
  val base1 = Base1(a + b)
  
  fun foo() = with(Base1) { baz() }
  fun baz() = base.baz()
  fun test() = base1.foo()
}

Which is exactly how it is implemented :)

As for the "Exports" feature (aspect two in this discussion), does the user really need to know about the implementation Detail (delegation) how Derived implements the method baz() ?

@fatjoem
Copy link

fatjoem commented Jul 30, 2019

I agree with your idea of inserting implicit with statements. I cannot tell whether that is the best solution but it seems a reasonable enough idea.

I did not mean to introduce real extension functions. I just meant that the proposal seems conceptually very similar to implicit extension functions, especially if we had compound receiver extension functions.

I find the similarity to extension functions greater than the similarity to companion objects.

I also might have used the term "receiver" incorrectly, whereas in fact I meant "context".

I still don't think that this proposal could replace compound extensions. See my comment in the other keep for a use case that needs both proposals (#176 (comment)).

@hannomalie
Copy link

Hm, i think it indeed can. Take a look at https://github.com/hannespernpeintner/kotlin/blob/keep-106/compiler/testData/codegen/box/companionval/CompanionAsFunctionArg.kt

Which could easily be extended to a fun that takes two or more companion params and make them available in the body.

If i weren't to bad to implement it, i would have made a better implementation example with lambdas :)

@fatjoem
Copy link

fatjoem commented Jul 30, 2019

But that would greatly increase boilerplate at the call site, because I would need to provide the arguments explicitly. The whole purpose of the extension function is simplifying the call site. I don't need to provide the receiver because it is already available in call site's context (see how homeLink() function is invoked in my example).

@hannomalie
Copy link

hannomalie commented Jul 30, 2019

Uhh, now i got it. We're back at scala implicits completely i fear.

It was Not my intent to make parameters other then the single true receiver "Passed" in implicitly. That's also what keep-87 avoids in order to be not unmaintainable :) companions are used as implicits receiver at the use site, Not as implicit parameters at the call Site.

What you call boilerplate, i call proper code, because i don't want parameters to be passed in (besides type classes but that's a different story that avoids arbitrary scopes). Your example only works because your scope and your receiver are the same context and your receiver implements proper interfaces. In my example aren't any interfaces.

Edit: and the whole thing about extension functions is not to make the receiver parameter on the call site invisible, but instead visible as a receiver. Companion vals are made visible as a receiver context, because they are companion params in a function for example, Not a receiver parameter, as in compound receiver proposal.

@fatjoem
Copy link

fatjoem commented Jul 30, 2019

My example forces me to expose public properties, whereas I normally would like to make them private. I would not call that proper. Yes, the encapsulation could be fixed in plain Kotlin, but it would involve even more boilerplate code. It could be much simpler, cleaner and more flexible by combining the proposal in this keep with the compound extension proposal.

Disclaimer: I don't like Scala's implicit arguments that much. But the reason for me is that it would be more idiomatic in Kotlin to use compound extensions instead. They would work very well together with this keep's proposal in an idiomatic Kotlin way.

@fatjoem
Copy link

fatjoem commented Jul 30, 2019

I now updated my example to take advantage of companion values together with compound extensions: #176 (comment)

In my opinion the benefit of combining both proposals is very clear.

@fatjoem
Copy link

fatjoem commented Jul 30, 2019

I believe that this keep together with compound extension functions ( #176) 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 here and here.

The classical monoid example would go like this, when using companion values:

interface IMonoid<A> {
  fun zero(): A
  fun A.append(other: A): A
}

class StringMonoid : IMonoid<String> {
  override fun zero() = ""
  override fun String.append(other: String) = this + other
}

fun <A> IMonoid<A>.doMonoidStuff(a: A) {
    ...
}

fun main() {
  // the companion val can go into different scopes.
  // in this example we are scoping it locally in this function:
  companion val stringMonoid = StringMonoid()

  val someString = "hi world"
  doMonoidStuff(someString)
}

@fatjoem
Copy link

fatjoem commented Aug 18, 2020

For comparison, there exists a similar feature in the language Xtend. It is called "extension provider" there. It also supports extension imports.

https://www.eclipse.org/xtend/documentation/202_xtend_classes_members.html#extension-provider
https://www.eclipse.org/xtend/documentation/202_xtend_classes_members.html#extension-imports

@elizarov
Copy link
Contributor

#114 (comment)

@elizarov elizarov closed this Dec 16, 2020
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.

6 participants