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

Default values for properties in interfaces #102

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

MarcinMoskala
Copy link

@MarcinMoskala MarcinMoskala commented Mar 26, 2018

Just as we can have default bodies for methods in interfaces, we should be able to give default values for properties:

interface MyTrait {
    
    var items: List<Int> = emptyList()
    
    fun isEmpty(): Boolean {
        return items.isEmpty()
    }
}

Under the hood it can work just as default bodies for methods - the compiler can use default property if the property is not overridden.

@udalov
Copy link
Member

udalov commented Mar 26, 2018

What's the problem with declaring the getter explicitly?

interface MyTrait {
    val items: List<Int>
        get() = emptyList()

    fun isEmpty(): Boolean = items.isEmpty()
}

@gildor
Copy link
Contributor

gildor commented Mar 27, 2018

Under the hood it can work just as default bodies for methods

Such approach completely changes semantics of property, property with field now behaves as property with getter.
Much better to use getter with default value instead

@MarcinMoskala
Copy link
Author

MarcinMoskala commented Mar 27, 2018

@udalov Because it is not mutable. It is important in cases when we need to use properties with underlying fields.

@gildor
Copy link
Contributor

gildor commented Mar 28, 2018

@MarcinMoskala Interfaces cannot have states (and mutable field in an interface is a state), so I suppose this is just impossible. You can use some global state to save default value.
Maybe you could provide some practical example why do you need this

@MarcinMoskala
Copy link
Author

Interfaces cannot have methods either. But if you compile and decompile to Java this file:

interface I {

    fun a(): String {
        return "Bla bla bla"
    }
}

You will find:

public interface I {
   @NotNull
   String a();

   @Metadata(
      mv = {1, 1, 9},
      bv = {1, 0, 2},
      k = 3
   )
   public static final class DefaultImpls {
      @NotNull
      public static String a(I $this) {
         return "Bla bla bla";
      }
   }
}

There is static function for every method with default body. When you use this interface:

class J: I

Under the hood, method is implemented and filled with default body:

public final class J implements I {
   @NotNull
   public String a() {
      return I.DefaultImpls.a(this);
   }
}

The same mechanism can be easily applied for properties. Property is getter or getter and setter. Property in interface needs to be overridden. Although it has a default value, then the class that implements it should make a property with the default value. It should be intuitive that this:

interface I {
    var a = 10
}

class A: I
class B: I
class C: I

Should work the same as this:

interface I {
    var a: Int
}

class A: I {
    override var a = 10
}
class B: I {
    override var a = 10
}
class C: I {
    override var a = 10
}

To find use cases, just look at any "Base" classes. They are sometimes huge and they have a lot of different responsibilities. We would strongly prefer having smaller classes with separate responsibilities.

@udalov
Copy link
Member

udalov commented Mar 28, 2018

Let's replace 10 in your example with a method call, e.g. computeA():

interface I {
    var a = computeA()
}

Now, there are many questions:

  1. Where is the bytecode for computeA() generated? Is it duplicated in each subclass or is it in a static method called from subclasses? How would we obtain the body if the interface is compiled separately?
  2. What's the scope of the expression from the point of view of resolution, i.e. what names can I use in the computeA() call and how are they made available in the generated bytecode? Can I do this?
interface I {
    var b = computeB()
    var a = computeA(b)
}

or this?

interface I {
    var a = computeA(b)
    var b = computeB()
}
  1. At what point precisely is computeA() called? I suppose it's in the constructor of each subclass. But a subclass may have a superclass already. Should this "superinterface initializer" code be called before the superclass constructor or after?
open class A {
    init {
        // ...
    }
}

class B : A(), I {
    // is A's constructor or I's "initializer" called first?
}

class C : I, A() {
    // how about here?
}
  1. What about any non-trivial hierarchies where the same interface can be present multiple times: is this "initializer" invoked for each occurrence of the interface in the hierarchy, or only once? What values are used for the properties?
var state = 0
fun computeA() = ++state

interface I {
    var a = computeA()
}

interface A : I
interface B : I

class C : A, B {
    // is a 1 or 2 here?
}
  1. What if in the hierarchy, there's already a superclass that has its property generated in such a way from the same interface? Should we generate another instance of it or use the one from the superclass? How would we know, looking at the .class file, if the superclass had used this generation scheme or its property was implemented manually?
interface I {
    var a = computeA()
}

open class A : I

class B : I, A() {
    // should we generate override var a here or not? should we just call computeA and ignore the result?
}

@MarcinMoskala
Copy link
Author

Thank you @udalov. These are very good questions. This is how I see it:
ad 3. Since interfaces are declared after superclass, it should be clear that their initialization should take place after superclass initialization. All fields need to be generated before everything else in the class. So this:

interface A {
    val a = 1
}
class B: A {
    val b = a
}

Should give the same result as this:

interface A {
    val a: Int
}
class B: A {
    val a: Int = 1
    val b = a
}

ad 2. This scope should be similar as in classes.
ad 4, 5. Same as for default bodies for methods:

interface I {
    fun f() = 1
}

class A: I {
    
}

class B: I {
    override fun f() = 3
}


fun main(args: Array<String>) {
    print(B().f()) // 3
}

and also the same as in Interface Delegation: Default initializer should be used only if the property is not initialized yet.

ad 1. Default initialization is just a function, and this is how it should be held. So this:

interface I {
    var a = computeA()
}

Should be compiled to something like this:

interface I {
    public A getA()
    public void setA(A a)

    static A defailt$A() {
        return computeA()
    }
}

And in class, if the property is not initialized then there is generated initialization that fills value using default$A.

In general this interfaces can act like restricted classes or, if you prefer, as interface and delegate. So this example:

interface I {
    var a = computeA()
}

class B: I

Can be similar as:

class DefaultI: I {
    override var a = computeA()
}
interface I {
    var a: A
}
class B: I by DefaultI()

But much simpler to use.

@MarcinMoskala MarcinMoskala changed the title Propose default values for properties in interfaces Default values for properties in interfaces May 6, 2018
@stangls
Copy link

stangls commented Aug 2, 2018

I think there are some use-cases for small class hierarchies where this works great.

But still I have some problem understanding what happens if for example an interface wants to count the number of instances of itself like this:

var counter = 0
interface I {
    var count = run{ counter++; counter }
}

open class A : I
class B : A(), I

fun main(args: Array<String>) {
	println( B().count ) // outputs 1 or 2?
	println( B().count ) // outputs 2 or 4?
}

I think it would be more reasonable to not extend interfaces like this but to provide real traits.

@MarcinMoskala
Copy link
Author

What I propose is a default initialization method that it is used only if no other method is defined. In this case we don't have any conflicting implementations so we know how this property should be initialized. Generated B should be the same as:

class B : A(), I {
    var count = run{ counter++; counter }
}

So the output should be 1 and 2. The same as for methods:

interface I {
    fun f() = 10
}

open class K : I
class M: K(), I

When we have conflicting properties with different default initialization methods, we should be forced to override such member. This is the same problem we have with default bodies.

zrzut ekranu 2018-08-03 o 16 49 48

@Mishkun
Copy link

Mishkun commented Sep 19, 2018

@MarcinMoskala can you provide a useful usecase which cannot be implemented using current interface declaration rules?

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.

5 participants