Mixin

December 20, 2021

Toy Lang 支援物件個體化(Object individuation),也就是類別的實例建立之後,還可以動態地增減其特性,那麼類別呢?

Toy 語法

類別在定義之後,若需要,也可以動態地增加方法的定義,例如:

class Foo {
    def doFoo1() {
        println('{0} foo1'.format(this))
    }
}

def doFoo2() {
    println('{0} foo2'.format(this))
}

println(Foo.ownMethods())

foo = new Foo()
foo.doFoo1()

Foo.addOwnMethod('doFoo2', doFoo2)
println(Foo.ownMethods())
foo.doFoo2()

Foo.deleteOwnMethod('doFoo2')
println(Foo.ownMethods())

在 Toy Lang 中,類別是 Class 的實例,Class 定義了 ownMethods 可取得類別上定義之方法,可以藉由 addOwnMethod 來動態加入方法,由於實例查找方法時,會到類別上尋找,因此建立的實例馬上也能使用被增加的方法,若要刪除方法,可以使用 deleteOwnMethod。執行的結果如下:

[<Function doFoo1>]
<Foo object> foo1
[<Function doFoo1>,<Function doFoo2>]
<Foo object> foo2
[<Function doFoo1>]

這跟在實例上新增方法不同,實例上新增的方法屬於實例本身擁有,而類別上增加的方法並不屬於實例,而是屬於類別。

如果想要直接從另一個類別上頭「借用」方法,可以使用 Classmixin 方法,例如:

class Foo1 {
    def doFoo1() {
        println('{0} foo1'.format(this))
    }
}

class Foo2 {
    def doFoo2() {
        println('{0} foo2'.format(this))
    }
}

Foo1.mixin(Foo2)

foo1 = new Foo1()
foo1.doFoo2()

mixin 方法會找出被 Mixin 的類別上定義之方法,讓呼叫 mixin 的類別內部,也能參考那些方法。執行結果如下:

class Foo1 {
    def doFoo1() {
        println('{0} foo1'.format(this))
    }
}

class Foo2 {
    def doFoo2() {
        println('{0} foo2'.format(this))
    }
}

Foo1.mixin(Foo2)

foo1 = new Foo1()
foo1.doFoo2()

這就形成了有趣的應用,可以將一組可共用的方法,定義在某個類別之上,例如:

class Ordered {
    def lessThan(that) {
        return this.compare(that) < 0
    }

    def lessEqualsThan(that) {
        return this.lessThan(that) or this.equals(that)
    }

    def greaterThan(that) {
        return not this.lessEqualsThan(that)
    }

    def greaterEqualsThan(that) {
        return not this.lessThan(that)
    }
}

class Circle {
    def init(radius) {
        this.radius = radius
    }

    def compare(that) {
        return this.radius - that.radius
    }

    def equals(that) {
        return this.radius == that.radius
    }
}

Circle.mixin(Ordered)

c1 = new Circle(10)
c2 = new Circle(20)

println(c1.lessThan(c2))          # true
println(c1.lessEqualsThan(c2))    # true
println(c1.greaterThan(c2))       # false
println(c1.greaterEqualsThan(c2)) # false

在這個例子中,Ordered 定義了一組可共用的方法,這些方法依賴在一個未實作的 compare 方法,如果需要比較操作的物件,在定義類別時可定義 compare 方法,並 mixin 這個 Ordered,就可以自動擁有已實作的比較操作。

在 Toy Lang 中,Mixin 並不是繼承,後面會看到,繼承時子類別並不擁有 Parent 類別的方法,而是透過繼承鏈查找,而 Mixin 時,類別會與被 Mixin 的類別共用方法。

Toy 實作

在 Toy Lang 中,每個物件都使用 Instance 節點來表示,而每個 Instance 必須有個內部節點表示:

class Instance extends Value {
    constructor(clzOfLang, properties, internalNode) {
        super();
        this.clzOfLang = clzOfLang; 
        this.properties = properties;
        this.internalNode = internalNode || this;
        this.value = this;
    }

    ...
}

對於類別,InstanceinternalNode 參考的是 Class 節點(這不是 Toy Lang 語言中的 Class):

class Class extenads Func {
    constructor({notMethodStmt, methods, name, parentClzNames, parentContext}) {
        super([], notMethodStmt, name, parentContext || null);
        this.parentClzNames = parentClzNames || ['Object'];
        this.methods = methods;
    }

    methodArray() {
        return this.methods.values();
    }

    addOwnMethod(name, fInstance) {
        this.methods.set(name, fInstance.internalNode);
    }

    deleteOwnMethod(name) {
        this.methods.delete(name);
    }

    hasOwnMethod(name) {
        return this.methods.has(name);
    }    

    hasMethod(context, name) {
        if(this.name === 'Object') {
            return this.hasOwnMethod(name);
        }

        return this.hasOwnMethod(name) || 
                this.parentClzNames.some(clzName => clzNode(context, clzName).hasOwnMethod(name)) ||
                grandParentClzNames(context, this.parentClzNames).some(
                    clzName => clzNode(context, clzName).hasMethod(context, name)
                );
    }

    getOwnMethod(name) {
        return this.methods.get(name);
    }

    getMethod(context, name) {
        const ownMethod = this.getOwnMethod(name);
        if(this.name === 'Object') {
            context.RUNTIME_CHECKER.refErrIfNoValue(ownMethod, name);
        }

        return ownMethod ? ownMethod : lookupParentClzes(context, this, name);
    }

    ...
}

每個 Class 節點,都有 methods 特性,包含了類別上定義之方法,類別的實例查找方法時,就是查找 methods 中是否有需要之方法,若沒有,才會到 Parent 類別尋找。

因此,只要操作 methods,就可以改變類別上擁有之方法,這也就是 Class 中看到的:

...
    addOwnMethod(name, fInstance) {
        this.methods.set(name, fInstance.internalNode);
    }

    deleteOwnMethod(name) {
        this.methods.delete(name);
    }
...

至於 mixin 方法,當然就是查找被 Mixin 類別之 Class 節點中之 methods,將其中的方法一次性地,增添到呼叫 mixin 的類別中,這可以在 clz.js 看到:

['mixin', func1('mixin', {
    evaluate(context) {
        Array.from(PARAM1.evaluate(context).internalNode.methodArray())
                .forEach(f => selfInternalNode(context).addOwnMethod(f.name, f.evaluate(context)));
        return context.returned(self(context));
    }    
})],

因此在實作上,Mixin 比繼承來得簡單多了,因為後面會看到,Toy Lang 支援多重繼承,在繼承鏈的查找上,會有多個來源,這點麻煩許多,Mixin 嘛!直接共用就對了!