Built-in mixins

I’m curious as to people’s thoughts concerning mixins for Lucee 5 classes. Being aware of using included templates as mixins is possible, I’m more interested in an object-oriented approach to mixins. My experience with existing other languages/frameworks with mixin capability is limited to PHP Traits, and ExtJS 4+ mixins.

I am somewhat partial to the way PHP does traits, therefore my ideal is very similar to them, with some minor differences. Some initial thoughts on what I think built-in mixin support looks like are as follows:

  • Mixins are to be defined as classes. Preferably abstract classes.
  • Mixins are loaded into other class definitions using a new keyword. The syntax should follow the form of the short-hand syntax property definition for components.
  • Any constructors are ignored when adding a mixin to a class.
  • Classes do not gain any type information from applied mixins.
  • Mixin methods are copied into the destination class’ definition.
  • Class-defined methods have priority over mixin methods.
  • Utilized mixins are to be individually and specifically accessible, in a protected manner (available privately and to sub-classes, but not publicly).
  • A class that implements two mixins with the same method name must define its own implementation of that method, otherwise generate a compile-time exception.

An example:

// app/mixins/FearMixin.lucee
abstract class FearMixin {
    public function screamIrrationally(message) {
        writeoutput(ucase(message) & "!!!");
    }
    public function shakeHands() {
        this.hands.tremble();
        this.screamIrrationally("run for your lives");
    }
}
// app/mixins/CordialMixin.lucee
abstract class CordialMixin {
    public function greetCalmly(message) {
        writeoutput("Greetings, I would like to say " & message);
    }
    public function shakeHands() {
        this.hands.rightHand.extend();
        this.greetCalmly("put 'er there, partner!");
    }
}

class ConflictedIndividual {
    use app.mixins.FearMixin fearMixin;
    use app.mixins.CordialMixin cordialMixin;

    public function shakeHands() {
        // access the correct mixin individually based on whether or not the individual is scared at the time.
        if (this.isScared) {
            fearMixin.shakeHands();
        } else {
            cordialMixin.shakeHands();
        }
    }
}

individual = new ConflictedIndividual();

assertFalse(isInstanceOf(individual, "app.mixins.FearMixin"));

I’m putting this here for discussion before creating a proposal issue on Jira. So… thoughts?

1 Like

I think it’s a good idea, but your suggested implementation seems a bit lacking. I’m splitting my time 90/10 PHP/CFML these days, and am getting a good handle on PHP’s traits.

I have previously written-up my findings here - Adam Cameron's Dev Blog: Looking at PHP's OOP from a CFMLer's perspective: traits - and having scanned through that it still represents my position now that I’m using PHP all day every day.

Things I don’t like about your suggested implementation:

No. They are a separate thing. It’s confusing to suggest they’re classes as they can’t work like classes, and aren’t intended to.

They don’t have a sense of their own state, so I don’t think new is the correct statement here. I think use makes sense, and also has a good precedent set elsewhere.

There’s no reason to not allow different access levels, either specified by the mixin, or by the class mixing it in (see code example below).

It’s completely wrong to suggest they should not be public, as one reason for using a trait is to fulfil an interface. Which implicitly means the methods are public.

This seems like an artificial restriction to me. It’s easy enough to work around name collisions via PHP’s approach:

use Serialisation, Logging {
	toXml        as private _toXml;
	toJson        as private;
	logToFile    as private;
	logToScreen    as private;
}

(where in this case Serialisation and Logging are both traits)

I agree that if there is a name collision, it’d be reasonable for some sort of error to arise.

Still: it’s def a good idea. I’m glad someone is bringing language discussion to this forum.


Adam

Adam, thanks for the input - I do need to provide some clarification though

I don’t see a reason why a mixin couldn’t be a class that can be instantiated. It’s just when a class gets mixed into another, you do not get the type inheritance/construction that you would normally get by creating an instance. This is an aspect that I borrowed from ExtJS’s mixin system.

I meant “new” in the sense of a yet-to-be-determined keyword, not the actual new keyword… I also like the use keyword, just thought I’d leave it open to any suggestions.

Again, this is a misunderstanding (due to my lack of clarity I’m afraid). The methods defined in the mixin absolutely should keep their defined access level. However, what I’m referring to is providing a private/protected key to access the original mixin as it was defined from within the utilizing class. I give example of that in my overridden shakeHands() method where I conditionally call fearMixin.shakeHands(); or cordialMixin.shakeHands();

That is actually more a concept from Java 8’s default method implementation in interfaces. In the instance that you use 2 mixins that both define a public method with the same name, you have to have some way of knowing which to use. There are 3 options:

  1. specify which functions to use from the mixin in the use statement, such as in PHP
  2. the version from the last mixin to define, in order it was use’d, is the one that would be executed
  3. explicitly provide the override, that can call the intended method as desired.

I happen to prefer the last option, as it is the most explicit in terms of what is going on.

Here’s an explanation of ExtJS mixins. It may help in understanding part of where I’m coming from.

CFML classes are more similar to JS objects in the way that JS method functions can be copied from one prototype to another. That’s why I do not see a problem with mixins being regular classes.

And here’s a simple code example:

Ext.define('CoolPerson', {
	extend:'Person',
	
	mixins: {
		canPlayGuitar:'CanPlayGuitar',
		canSing:'CanSing'
	},
	
	sing: function(){
		alert("Ahem...");
		this.mixins.canSing.sing.call(this);
		this.playGuitar();
		
	}
});
1 Like

For future reference, ACF compatibility with default interface methods is being implemented in LDEV-1835

1 Like