你定義了一個 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) {
    ...
}
在 ToyLang 中,查找類別的方法時,最上層的查找終點,都是 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。
在 ToyLang 中,可以進行多重繼承,這個時候要注意搜尋的順序,是從 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
在 ToyLang 中,類別都是 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 方法。
在實作出語言的類別功能之後,緊接而來的需求就是,你會發現有些方法,在某類別上會需要,在另一個類別上也會需要,解決的方式之一是透過 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
之前曾經談過,如果語言不用支援物件導向,實作上就會簡單許多,那麼為什麼物件導向實作麻煩呢?與其說麻煩,不如說,很容易搞不清楚,你現在處理的是實作層面的元素,還是語言層面的元素,或者是你寫的程式中的元素。
ToyLang 中定義的類別,實作層面有個 Class 節點,這是語法樹節點,ToyLang 中每個使用 class 定義出來的類別,類別名稱、方法等,都是由 Class 節點的實例管理著。
ToyLang 中每個類別,都會是 ToyLang 中 Class 的實例,例如,ToyLang 中每個物件都可以呼叫 class 方法,取得建構該物件的類別,像是若 acct = new Account('123', 'Justin'),acct.class() 就會取得 Account,那麼 Account.class() 呢?會取得 ToyLang 中的 Class(不是語法節點的 Class)。
ToyLang 中每個物件對應的語法節點是 Instance,例如,acct = new Account('123', 'Justin') 的 acct 參考的物件,實作面上就是使用 Instance 的實例來表示。
每個 Instance 的實例,都會有 clzOfLang 特性,代表它是 ToyLang 中哪個類別建構而來,例如方才 acct 參考的物件,實作面上的 Instance 實例,clzOfLang 會是 Account 的實例,有趣的是,這個實例,在實作面上也是使用 Instance 的實例來表示,也就是 clzOfLang 參考的,也是個 Instance 的實例。
還有一個有趣的問題,ToyLang 中 Class 是個類別,它會是誰的實例?當然也是 Class!那在語法節點上,ToyLang 中 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;
搞清楚了嗎?畫一條線,左邊是語法樹,右邊是 ToyLang,兩邊要有什麼,各對應自什麼,都要搞清楚,這個活像是雞生蛋、蛋生雞的問題,在實作類別時會遇上一次,在實作繼承時又會遇上一次!
也就是 … Object 是類別,因此它必須是 Class 的實例,然而 Class 的 Parent 類別是 Object,這 … XD
實際上這是發生在實作層面的關係組合!如果你曾經接觸過 meta programming 之類的機制,像是 Java 的反射,就會遇上類似的事情,追溯到某個層面,再要往上就是原生實作的部份了。
就 ToyLang 的實作面,也就是這個部份:
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 之間找尋。

