Mixin
December 20, 2021Toy 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>]
這跟在實例上新增方法不同,實例上新增的方法屬於實例本身擁有,而類別上增加的方法並不屬於實例,而是屬於類別。
如果想要直接從另一個類別上頭「借用」方法,可以使用 Class
的 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()
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;
}
...
}
對於類別,Instance
的 internalNode
參考的是 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 嘛!直接共用就對了!