繼承
December 20, 2021在 Toy Lang 中,繼承從定義類別的一開始就存在了!
Toy 語法
你定義了一個 Account
類別,雖然沒有聲明,實際上已經包含了繼承:
class Account {
def init(number, name) {
this.number = number
this.name = name
this.balance = 0
}
def deposit(amount) {
if amount <= 0 {
throw new Exception('存入必須是正數')
}
this.balance += amount
}
def withdraw(amount) {
if amount > this.balance {
throw new Exception('餘額不足')
}
this.balance -= amount
}
def toString() {
return '{0}, {1}, {2}'.format(this.number, this.name, this.balance)
}
}
上面的例子其實相當於:
class Account(Object) {
...
}
在 Toy Lang 中,查找類別的方法時,最上層的查找終點,都是 Object
類別,因此,只要是 Object
上的方法,像是 ownProperties
、hasOwnProperty
、deleteOwnProperty
等方法,Account
的實例都可以使用:
acct = new Account('123', 'Justin')
# 顯示 [[number,123],[name,Justin],[balance,0]]
println(acct.ownProperties())
若要指定繼承的類別,是在類別名稱旁使用括號表明要繼承的 Parent 類別。例如,你為以上的類別建立了一個支票帳戶:
class CheckingAccount(Account) {
def init(number, name) {
# 呼叫 Parent 類別 init()
this.super(Account, 'init', [number, name])
this.overdraftlimit = 30000
}
def withdraw(amount) {
if amount <= self.balance + self.overdraftlimit {
self.balance -= amount
}
else {
throw new Exception('超出信用')
}
}
def toString() {
return this.super(Account, 'toString') + ', ' + this.overdraftlimit
}
}
在上例中,繼承了 Account
來定義 CheckingAccount
類別。如果在 Child 類別中,需要呼叫 Parent 類別的某個方法,則可以使用 Object
繼承下來的 super
方法,指定類別與方法名稱,以及呼叫時的引數清單。
在上例中,重新定義了 withdraw
與 toString
方法,在操作實例方法時,是從類別上開始尋找是否有定義,否則就搜尋 Parent 類別中是否有定義方法:
acct = new CheckingAccount('E1234', 'Justin Lin')
println(acct)
acct.deposit(1000) # 使用 Account 的 deposit() 定義
println(acct)
acct.withdraw(2000) # 使用 CheckingAccount 的 withdraw() 定義
println(acct)
在呼叫 acct
的 deposit
方法時,由於 CheckingAccount
並沒有定義,因此呼叫的是 Account
類別上定義的 deposit
,而呼叫 withdraw
時,使用 CheckingAccount
有定義的 withdraw
。
在 Toy Lang 中,可以進行多重繼承,這個時候要注意搜尋的順序,是從 Child 類別開始,接著是同一階層 Parent 類別由左至右搜尋,再至更上層同一階層父類別由左至右搜尋,直到達到頂層的 Object
為止。例如:
class A {
def method1() {
println('A.method1')
}
def method2() {
println('A.method2')
}
}
class B(A) {
def method3() {
println('B.method3')
}
}
class C(A) {
def method2() {
println('C.method2')
}
def method3() {
println('C.method3')
}
}
class D(B, C) {
def method4() {
println('D.method4')
}
}
d = new D()
d.method4() # 在 D 找到,D.method4
d.method3() # 以 D->B 順序找到,B.method3
d.method2() # 以 D->B->C 順序找到,C.method2
d.method1() # 以 D->B->C->A 順序找到,A.method1
在 Toy Lang 中,類別都是 Class
的實例,Class
定義了 parents
方法,可取得繼承的 Parent 類別清單:
# 顯示 [<Class B>,<Class C>]
println(D.parents())
parents
方法也可以用來動態地改變繼承的 Parent 類別。例如:
class BB(A) {
def method3() {
println('BB.method3')
}
}
parents = D.parents()
parents.set(0, BB)
D.parents(parents)
# 顯示 BB.method3
d.method3()
在上例中,D
原本來繼承 B
與 C
類別,透過 parents
方法修改為繼承自 BB
與 C
類別,因此尋找 method3
方法時,也就改尋找 BB
類別,因此最後執行的是 B
的 method3
方法。
Toy 實作
在實作出語言的類別功能之後,緊接而來的需求就是,你會發現有些方法,在某類別上會需要,在另一個類別上也會需要,解決的方式之一是透過 Mixin,另外傳統上常見的方式之一就是繼承。
在沒有指定繼承的對象時,預設會是 Object
,這實作在 line_parser.js 中:
function createAssignClass(tokenableLines, argTokenable) {
const [fNameTokenable, ...paramTokenables] = argTokenable.tryTokenables('func');
fNameTokenable.errIfKeyword();
const remains = tokenableLines.slice(1);
const stmt = LINE_PARSER.parse(remains);
const clzLineCount = stmt.lineCount + 2;
const parentClzNames = paramTokenables.map(paramTokenable => paramTokenable.value);
const [fs, notDefStmt] = splitFuncStmt(stmt);
return new StmtSequence(
new ClassStmt(
Variable.of(fNameTokenable.value),
new Class({
notMethodStmt : notDefStmt,
methods : new Map(fs),
name : fNameTokenable.value,
// 預設繼承 Object
parentClzNames : parentClzNames.length === 0 ? ['Object'] : parentClzNames
})
),
LINE_PARSER.parse(tokenableLines.slice(clzLineCount)),
tokenableLines[0].lineNumber
);
}
類別是查找方法的依據,在實作繼承機制下的方法查找,就必須搞定查找順序,這也是 value.js 中 lookupParentClzes
在做的事情:
function lookupParentClzes(context, clz, name) {
// BFS
const parentClzName = clz.parentClzNames.find(
clzName => clzNode(context, clzName).hasOwnMethod(name)
);
if(parentClzName) {
return clzNode(context, parentClzName).getOwnMethod(name);
}
const grandParentClzName = grandParentClzNames(context, clz.parentClzNames).find(
clzName => clzNode(context, clzName).hasMethod(context, name)
);
context.RUNTIME_CHECKER.refErrIfNoValue(grandParentClzName, name);
const grandParentClzNode = clzNode(context, grandParentClzName);
const method = grandParentClzNode.getOwnMethod(name);
if(method) {
return method;
}
return lookupParentClzes(context, grandParentClzNode, name);
//return clzNode(context, grandParentClzName).getOwnMethod(name);
}
function clzNode(context, clzName) {
return context.lookUpVariable(clzName).internalNode;
}
function grandParentClzNames(context, parentClzNames) {
return parentClzNames.filter(clzName => clzName !== 'Object') // Object is the top class. No more lookup.
.map(clzName => clzNode(context, clzName))
.map(clzNode => clzNode.parentClzNames)
.reduce((acct, grandParentClzNames) => acct.concat(grandParentClzNames), [])
}
因為想要故意挑戰多重繼承,因而查找上必須多費些功夫,這時就覺得函數式程式設計真是好用啊!遞迴時只要處理當時的任務,下個遞迴呼叫就不用理了… XD
之前曾經談過,如果語言不用支援物件導向,實作上就會簡單許多,那麼為什麼物件導向實作麻煩呢?與其說麻煩,不如說,很容易搞不清楚,你現在處理的是實作層面的元素,還是語言層面的元素,或者是你寫的程式中的元素。
Toy Lang 中定義的類別,實作層面有個 Class
節點,這是語法樹節點,Toy Lang 中每個使用 class
定義出來的類別,類別名稱、方法等,都是由 Class
節點的實例管理著。
Toy Lang 中每個類別,都會是 Toy Lang 中 Class
的實例,例如,Toy Lang 中每個物件都可以呼叫 class
方法,取得建構該物件的類別,像是若 acct = new Account('123', 'Justin')
,acct.class()
就會取得 Account
,那麼 Account.class()
呢?會取得 Toy Lang 中的 Class
(不是語法節點的 Class
)。
Toy Lang 中每個物件對應的語法節點是 Instance
,例如,acct = new Account('123', 'Justin')
的 acct
參考的物件,實作面上就是使用 Instance
的實例來表示。
每個 Instance
的實例,都會有 clzOfLang
特性,代表它是 Toy Lang 中哪個類別建構而來,例如方才 acct
參考的物件,實作面上的 Instance
實例,clzOfLang
會是 Account
的實例,有趣的是,這個實例,在實作面上也是使用 Instance
的實例來表示,也就是 clzOfLang
參考的,也是個 Instance
的實例。
還有一個有趣的問題,Toy Lang 中 Class
是個類別,它會是誰的實例?當然也是 Class
!那在語法節點上,Toy Lang 中 Class
的實例,也會有個 Instance
節點實例代表,那這個 Instance
的 clzOfLang
會是誰呢?
答案就是自己,這實作在 classes.js 中 …XD
const CLZ = ClassClass.classInstance(null, clzNode({name : 'Class', methods : ClassClass.methods}));
// 'Class' of is an instance of 'Class'
CLZ.clzOfLang = CLZ;
搞清楚了嗎?畫一條線,左邊是語法樹,右邊是 Toy Lang,兩邊要有什麼,各對應自什麼,都要搞清楚,這個活像是雞生蛋、蛋生雞的問題,在實作類別時會遇上一次,在實作繼承時又會遇上一次!
也就是 … Object
是類別,因此它必須是 Class
的實例,然而 Class
的 Parent 類別是 Object
,這 … XD
實際上這是發生在實作層面的關係組合!如果你曾經接觸過 meta programming 之類的機制,像是 Java 的反射,就會遇上類似的事情,追溯到某個層面,再要往上就是原生實作的部份了。
就 Toy Lang 的實作面,也就是這個部份:
const CLZ = ClassClass.classInstance(null, clzNode({name : 'Class', methods : ClassClass.methods}));
// 'Class' of is an instance of 'Class'
CLZ.clzOfLang = CLZ;
const BUILTIN_CLASSES = new Map([
// Object 的 clzOfLang 是 CLZ
ClassClass.classEntry(CLZ, 'Object', ObjectClass.methods),
ClassClass.classEntry(CLZ, 'Function', FunctionClass.methods),
['Class', CLZ],
ClassClass.classEntry(CLZ, 'Module', ModuleClass.methods),
ClassClass.classEntry(CLZ, 'String', StringClass.methods),
ClassClass.classEntry(CLZ, 'List', ListClass.methods),
ClassClass.classEntry(CLZ, 'Number', NumberClass.methods, NumberClass.constants),
ClassClass.classEntry(CLZ, 'Traceable', TraceableClass.methods)
]);
CLZ
參考的會是語法節點 Instance
的實例,方才已經看過,CLZ
的 clzOfLang
就是自身,而上面可以看到,Object
的 clzOfLang
是 CLZ
,這就解釋了「Object
是類別,因此它必須是 Class
的實例」這件事,這是 Instance
與 Instance
節點之類的關係。
至於「Class
的 Parent 類別是 Object
」,這是 Class
與 Class
節點之間的關係,也就是 createAssignClass
時指定了 Parent 類別建立的關係,後續透過 lookupParentClzes
查找時,就是在 Class
與 Class
之間找尋。