uqbar-project / wollok

Wollok Programming Language
GNU General Public License v3.0
60 stars 16 forks source link

Mixins based inheritance #322

Closed javierfernandes closed 8 years ago

javierfernandes commented 9 years ago

Mixin

A mixin is a new construction similar to a class but:

Some other technical details

Here is the simplest mixin providing a new method

mixin Flies {
    method fly() {
        console.println("I'm flying")
    }
}

Then it can be mixed in a class

class Bird mixed with Flies {}

And later used in a program/test/library

program t {
    val b = new Bird()
    b.fly()
}

Mixin with state

Besides behavior (methods), a mixing can define instance variables.

mixin Walks {
    var walkedDistance = 0
    method walk(distance) {
        walkedDistance += distance
    }
    method walkedDistance() = walkedDistance
}
class WalkingBird mixed with Walks {}

Then used

val b = new WalkingBird()
b.walk(10)
assert.equals(10, b.walkedDistance())

Instance Variables Access

Instance variables declared by a mixin can be accessed from the the class it is mixed in into.

class WalkingBird mixed with Walks {
       method resetWalkingDistance() {
               walkedDistance = 0     // variable defined in the mixin
       }
}

Mixing multples mixins

A class can mix more than one mixin.

mixin M1 {}
mixin M2 {}

class C mixed with M1 and M2 {
}

The list of mixins can be separated either with "and" or with a comma character.

class C mixed with M1, M2  {}

Abstract Mixins

A mixin can be abstract if it calls a method which is not defined in itself. Here is an abstract Mixin providing flying capabilities

mixin Flying {
    var fliedMeters = 0
    method fly(meters) {
        this.reduceEnergy(meters)
        fliedMeters += meters
    }
    method fliedMeters() = fliedMeters
}

The method that it needs is reduceEnergy(meters).

When executed "this.reduceEnergy()" will send a message to the current object (this). This means that the method can be anywhere in the object's hierarchy and will be looked up for.

There are 3 possible cases

Method implemented in the class

class BirdWithEnergyThatFlies mixed with Flying {
    var energy = 100
    method energy() = energy
    method reduceEnergy(amount) {
        energy -= amount
    }
}

Method implemented in a superclass

class Energy {
    var energy = 100
    method energy() = energy
    method reduceEnergy(amount) {
        energy -= amount
    }
}

class BirdWithEnergyThatFlies inherits Energy mixed with Flying {
}

Here we can see that the new method lookup doesn't affect regular class-based inheritance.

Method defined in another mixin

It could be in a new mixin

class Energy {
    var energy = 100
    method energy() = energy
    method reduceEnergy(amount) {
        energy -= amount
    }
}

And then used

class BirdWithEnergyThatFlies mixed with Energy, Flying {}

The mixin order in this case is not relevant, since sending a message to this starts a new method lookup that starts bottom-up from the object's concrete type. We will see later that the declaration's order indeed is important for method look up in other cases

Linearization

Mixins are hooked up into the class hierarchy between classes following a linearization algorithm (This is basically the same way Scala traits/mixins works) This mechanism makes sure that there are no complex hierarchy graphs. It all ends up being a linear relation between classes and mixins. So the hierarchy is a simple List and the method and variables lookup mechanism only has an predictable flow up the chain with a simple step and not a split decision to take.

Here are some example linearizations

mixin M1 {
}
class A {}

class B inherits A mixed with M1 {}

B's hierarchy ends like this

B -> M1 -> A

If we add a new mixin:

class B inherits A mixed with M1, M2 {}

The new hierarchy would be

B -> M2 -> M1 -> A

**Notice here that the declaration order DOES MATTER. Mixins on the right side are lower in the hierarchy. Meaning that they have precedence over the ones on the left

Having mixins up in the class hierarchy doesn't complicate this at all, since each classes mixins are resolved in the same way

mixin M1 { ... }
mixin M2 { ... }
mixin M3 { ... }
mixin M4 { ... }
mixin M5 { ... }
mixin M6 { ... }

class A { ... }
class B inherits A mixed with M1, with M2 { ... }
class C inherits B mixed with M3 { ... }
class D inherits C mixed with M4, M5, M6 { ... }

D's inheritance chain is

D -> M6 -> M5 -> M4 -> C -> M3 -> B -> M2 -> M1 -> A

This was resolved class by class

Method Override

Understanding linearization is important to implement modular mixins which collaborate between each others without knowing themselves. We have seem some simple cases already. Here we present more complex cases where a class/mixin overrides a method

Class overrides mixins method

The class on which we are mixing the mixins has the biggest priority since it is the lower end of the hierarchy, that means that it can override a method defined in a mixin. In the same way that a class can override a method defined in a super class

Given this mixin

mixin Energy {
    var energy = 100
    method reduceEnergy(amount) { energy -= amount }
    method energy() = energy
}

A class can be mixed and override the "reduceEnergy(amount)" method

class Bird mixed with Energy {
    override method reduceEnergy(amount) { 
        // does nothing
    }
}

Super call (in a class overriding a mixin method)

As with any method overriding a method in a super class its body can use the super keyword to execute the original method being overriding.

    class Bird mixed with Energy {
        override method reduceEnergy(amount) { 
            super(1)
        }
    }

Super call in mixin

This is the most complex case and also the most flexible. A mixin can override a method and also use the original implementation. For that case the super works like a "dynamic dispatch" (just "like"). Because looking at the mixin code that calls super() is not enough to statically understand which method will be called by that "super" call. For example:

mixin M1 {
    method doFoo(chain) { super(chain + " > M1") }
}

We don't know what "super" means in this context. But we do know once the mixin is combined (statically) into a class

Given this class

class C1 {
    var foo = ""
    method doFoo(chain) { foo = chain + " > C1" }
    method foo() = foo
}

And this mixup

class C2 inherits C1 mixed with M1 { }

We now know that the "super" call in the mixin will call the "doFoo(chain)" method defined in the C1 class (C2's super class).

The way to understand this is that the linear hierarchy is built as we have seen already, and then "super" means to find the first method implementation right up the current mixin position

Here is the sample

C2 -> M1 (doFoo()) -> C2 (doFoo())
                    super --------->

Stackable Mixin Pattern

This means that the original method being overriden can be actually defined in another Mixing up in the hierarchy. And that mixin in turn could be overriding just to call super. So is like every mixin "decorates" or alters the original method behavior by reusing it, getting itself in the middle.

This is called the "stackable mixin pattern", because mixins can be combined by stacking them much like a chain of responsibilities pattern.

Here is the same example as before but with a couple more mixins

mixin M1 {
    method doFoo(chain) { super(chain + " > M1") }
}

mixin M2 {
    method doFoo(chain) { super(chain + "> M2") }
}

mixin M3 {
    method doFoo(chain) { super(chain + "> M3") }
}

And then the classes

class C1 {
    var foo = ""
    method doFoo(chain) { foo = chain + " > C1" }
    method foo() = foo
}

class C2 inherits C1 mixed with M1, M2, M3 {
}

Executing this code

    val c = new C2()
    c.doFoo("Test ")
    console.println(c.foo())

Prints the following

Test > M3 > M2 > M1 > C1 

Which is basically the linearized hierarchy

Object's Mixins

Named objects can also be combined with mixins

mixin Flies {
    var times = 0
    method fly() {
        times = 1
    }
    method times() = times
}

object pepita mixed with Flies {}

In this case the inheritance chain will be:

pepita -> Flies -> wollok.lang.Object

Limitations

Mixin inheritance

Current mixins implementation doesn't support a mixin extending another mixing or class. That's a difference from Scala mixins.

Native mixins

What if a mixin wants to implement a method natively ?

Combining mixins at instantiation time

This can be done in 1.6 !

javierfernandes commented 8 years ago

Related pending issues #536, #537, #538, #540, #541

javierfernandes commented 8 years ago

This is implemented in branch dev-mixins which is based on dev-splitting-plugins

javierfernandes commented 8 years ago

Another point to discuss is whether the required methods for a mixin must be explicitly declared as abstract methods in the mixin or just inferred by its usage. For example:

Implicitly

mixin M {
    method foo() {
        this.bar()
    }
}

Explicitly

mixin M {
    method foo() {
        this.bar()
    }
    method bar()
}

It currently works implicitly

javierfernandes commented 8 years ago

@npasserini @tesonep @flbulgarelli A couple of things to discuss here about mixins.

I have a current working solution in #dev-splitting-plugins branch (yeah, sorry, I need to refactor that branch, merging the split-plugins feature into dev, and then "renaming" the branch to "mixins"). Or cherrypick the mixins to a new branch from dev to decouple the changes. Damn I would really love to have a browser based version of wollok to have continuous deployment to try-out features in branches live :(

The impl es basically based on Scala mixins, which I think are pretty simples but powerful (it actually didn't involved much changes to the method dispatch and is fully transparent if you use just classes without mixins) Some other languages model mixins as extensions but they don't specify much on how to handle method overrides and things like stackable pattern.

The first part of this issue describes the current behavior. Then the linked issues, plus the "Limitations" opens questions on a couple of features that are not implemented.

javierfernandes commented 8 years ago

I have found that to make it easier checking object instantiation it would be good to have mixins required methods explicitly declared, like this

mixin FullName {
    method getFullName() = this.firstName() + ", " + this.lastName()

    method firstName()   // requirement
    method lastName()   // requirement
}

Instead of just

mixin FullName {
    method getFullName() = this.firstName() + ", " + this.lastName()
}

However if you think that we really should have the implicit implementation I can do a refactor to implement it.

flbulgarelli commented 8 years ago

Some comments:

flbulgarelli commented 8 years ago

Some more comments:

So, instead of writing

mixin FullName {
    method getFullName() = this.firstName() + ", " + this.lastName()

    method firstName()   // requirement
    method lastName()   // requirement
}

class Foo inherits Bar mixed with FullName

I suggest the following syntax:

mixin FullName {
    method getFullName() = this.firstName() + ", " + this.lastName()

    required method firstName() 
    required method lastName() 
}

class Foo inherits Bar includes FullName
javierfernandes commented 8 years ago

Ok to not allow a mixin to be mixed in.

You mean ok to the fact that a mixin cannot "inherit" from a class neither include (be mixed with) another mixins, right ?

I wonder why Scala uses this feature. I also think that it is difficult to grasp and confuses a lot. I haven't needed it yet in any example/test that I wrote.

Regarding "mixed with" I don't mind being verbose. On the contrary I have been reading a couple of other languages syntax that support mixins and they all use different terms, and none of them seemed expressive enough for me. I don't like Ruby's "include". Reminds me of "includes" from many other languages, like if it was an import or inlining.

I think that "mixed with" follows the core idea of the "mixin" that gets "mixed" in the hierarchy. Maybe "mixing" ?

class A inherits (from) B mixing M {  }

Regarding method requirements, I actually thought about modelling them with something like

requires firstName()
requires lastName()

But then I thought that it would add a new concept or somehow confuse. In some scenarios this methods behaves in the same way as any other abstract method (if you mix this you need to provide an implementation). The only difference with a regular "abstract method" in a class is that the implementation here could be down or up the hierarchy chain in this case.

Anyway, I guess that I have like "encountered feelings" about this. I like being expressive and if we think of this methods as requirements then have a keyword for that. But I liked the simplicity of treating them as regular methods.

Ok...okok.. lets add the keyword. :smile: Just that I guess the right one is require and not required

javierfernandes commented 8 years ago

Every time I wrote inherits I feel that we should go ahead and add the "from" too, to make it nicely readable in english. Is kind of weird now.

javierfernandes commented 8 years ago

Oh, we don't have an explicit abstract keyword, neither for classes nor methods. Again I like the fact that it requires less to write, but I agree that we are missing the opportunity to explicitly express there the intention. I think that @npasserini liked the "without abstract" keyword approach. I don't really mind adding it (we should add checks and quickfixes in that case)

flbulgarelli commented 8 years ago

Ok to not allow a mixin to be mixed in.

You mean ok to the fact that a mixin cannot "inherit" from a class neither include (be mixed with) another mixins, right ?

Right

Regarding "mixed with" I don't mind being verbose. On the contrary I have been reading a couple of other languages syntax that support mixins and they all use different terms, and none of them seemed expressive enough for me.

OK. Verbosity is not bad by itself. You have a point there. But

But really, let's get into spanish. Do we say "incluir"? "mezclar"? "extender"? I know I use the first term. What term do you use?

In English I think I tend to use include or mix-in. But mixing in a mixin is tongue twister :disappointed:

flbulgarelli commented 8 years ago

Oh, I think something like requires instead of required method could also work.

flbulgarelli commented 8 years ago

Regarding operational semantics of abstract and required methods, yeah, they are nearly the same at implementation level, but they are different concepts and we tend to present them different. That is why I think it is a good idea to have different keywords for them.

If we introduce abstract keyword, I would suggest to be consistent with the usage of an adjective in both cases: abstract and required. Otherwise, as stated in my previous comment, require(s) is ok.

That way the declaration is consisten with speech:

flbulgarelli commented 8 years ago

BTW, we are going to introduce this feature in 1.4. We can postpone syntax discussion