KEEP icon indicating copy to clipboard operation
KEEP copied to clipboard

Default values for properties in interfaces

Open MarcinMoskala opened this issue 6 years ago • 10 comments

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.

MarcinMoskala avatar Mar 26 '18 22:03 MarcinMoskala

What's the problem with declaring the getter explicitly?

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

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

udalov avatar Mar 26 '18 23:03 udalov

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

gildor avatar Mar 27 '18 00:03 gildor

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

MarcinMoskala avatar Mar 27 '18 21:03 MarcinMoskala

@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

gildor avatar Mar 28 '18 02:03 gildor

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.

MarcinMoskala avatar Mar 28 '18 11:03 MarcinMoskala

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?
}

udalov avatar Mar 28 '18 12:03 udalov

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 avatar Mar 29 '18 08:03 MarcinMoskala

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.

stangls avatar Aug 02 '18 21:08 stangls

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

MarcinMoskala avatar Aug 03 '18 15:08 MarcinMoskala

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

Mishkun avatar Sep 19 '18 15:09 Mishkun