if 陳述

December 18, 2021

if…else 這類語法,是語言中最基本的條件控制語法。

Toy 語法

要在某條件成立時才進行某些動作,Toy Lang 提供了 if 陳述,一個例子如下:

name = input('名稱:')
if name == '' {
    name = 'Guest'
}
println('Hello, {0}'.format(name))

這個範例中,使用 input 函式取得使用者輸入,如果使用者有輸入名稱,那麼 name 不會是空字串,也就是直接使用輸入之名稱來顯示 Hello 等訊息,否則 name 會被設定為 ‘Guest'

if 可以搭配 else,在 if 條件不成立時,執行 else 中定義的程式碼,上例也可以這麼寫:

name = input('名稱:')
if name == '' {
    println('Hello, {0}'.format('Guest'))
}
else {
    println('Hello, {0}'.format(name))
}

在 Toy Lang 中,區塊是使用右上 { 與左下 },雖然不要求縮排,然而為了可讀性,還是使用縮排吧!if...else 在撰寫時,一定要使用 {},不可省略,因此若有多重判斷,就會形成明顯的巢狀:

score = Number.parseInt(input('輸入分數:'))
if score >= 90 {
    println('得 A')
}
else {
    if score >= 80 and score < 90 {
        println('得 B')   
    }
    else {
        if score >= 70 and score < 80 {
            println('得 C')  
        }
        else {
            if score >= 60 and score < 70 {
                println('得 D')
            }
            else {
                println('不及格')
            }
        }
    }
} 

因為 {} 不可省略,不可能形成 C/C++、Java 那種 if...else if...else 的寫法,Toy Lang 也沒有提供 Python 中那種 if...elif...else,因為在我的想法中,那只是將巢狀變成瀑布罷了。

解決方法之一,就是改用之後會談到的 switch,方法之二就是視你的程式邏輯而定,適當地抽出獨立的邏輯成為函式,或者是其他的子元件吧!

在 Toy Lang 中有個 if...else 運算式語法,可以擁有像 C/C++、Java 的 ?: 三元運算子功能。例如:

name = input('名稱:')
println('Hello, {0}'.format('Guest' if name == '' else name))

以下是個簡單的判斷輸入數是奇數或偶數的程式:

input = Number.parseInt(input('輸入整數:'))
desc = '奇數' if input % 2 == 1 else '偶數'
println('{0} 為 {1}'.format(input, desc))

Toy 實作

在建立 if 陳述的節點時,主要必須有條件值、成立時要執行的陳述、不成立時要執行的陳述,因此節點會像是:

class If {
    constructor(cond, trueStmt, falseStmt) {
        this.cond = cond;
        this.trueStmt = trueStmt;
        this.falseStmt = falseStmt;
    }

    evaluate(context) {
        if(this.cond.evaluate(context).value) {
            return this.trueStmt.evaluate(context);
        }
        return this.falseStmt.evaluate(context);
    }   
}

看來簡單對吧!If 節點不用去管 trueStmtfalseStmt 是什麼,因為全部的陳述句節點,都會有相同的介面,只要判斷要執行哪個就好了,然而,只有 if 沒有 else 的情況呢?記得在〈變數與指定陳述〉中談到的 StmtSequence.EMPTY 嗎?如果有寫 if 而沒有寫 else,那麼 falseStmt 就指定為 StmtSequence.EMPTY

也就是說,if 陳述句節點比較像個容器,具有陳述句節點的介面,可以用來包括其他陳述句,並視情況選擇要執行的陳述句。

if 陳述句的難處,或者說 Toy Lang 中其他具有區塊結構的陳述句,它們的難處就在於,如何知道區塊的邊界,也就是說,難處還是在於 Parser。

首先如先前談過的,Toy Lang 為了簡化 Parser 的設計,採取了以行為單位進行剖析的策略,在 line_parser.js 中可以看到,STMT_PARSER 會逐一嘗試陳述句的模式,符合的話就建立對應的語法節點,然後繼續剖析剩餘的行。

對於指定陳述這類單行陳述來說,要繼續剩餘行的剖析,只要將索引前進一就可以搞定了,然而,有區塊的陳述句呢?如何能知道 if 的區塊有幾行呢?

這有點違反設計剖析器時,必須維持各個任務獨立性的感覺,畢竟,終要有個資料結構,記得已經剖析了 if 區塊中幾行程式碼,最後才能知道整個 if 區塊有幾行嘛!

然而,這個想法是絕對行不通的,因為 if 中還會有 if,甚至是 whileswitch,甚至是區域函式定義等,它們會以無限種方式組成,你想要的資料結構,是不可能考慮無限種可能的。

在土炮 Toy Lang 的過程,我曾經採取的尋找對稱的 {},然而,這方式最後是失敗的,原因之一是每增加一種陳述句,尋找對稱 {} 就要加入新的判斷,逐漸使得演算執來越複雜,原因之二就是上述的,演算本身已經沒有獨立性,最後也無法涵蓋各種可能的組合情況。

為什麼不讓各自的陳述句,記得各自擁有多少行呢?

class Stmt {
    constructor(lineCount) {
        this.lineCount = lineCount;
    }
}

那麼,StmtSequence.EMPTY 算是一行嗎?不!在 Toy Lang 的設計中,StmtSequence.EMPTY 只是用來組合 StmtSequence 時使用,並不是直接代表 },它還會用來代表上一個區塊結束,或者程式碼已結束,因此我讓 StmtSequence.EMPTYlineCount 為 0:

StmtSequence.EMPTY = {
    lineCount : 0,
    // We don't care about emtpy statements so the lineNumber 0 is enough.
    lineNumber : 0, 
    evaluate(context) {
        return context;
    }
};

因為每個陳述句節點,會記得自己有幾行,面對像 If 這種陳述句容器,就是將 trueStmtfalseStmt 加總起來:

function ifLineCount(trueStmt, falseStmt) {
    const trueLineCount = trueStmt.lineCount;
    const falseLineCount = falseStmt.lineCount;
    return 2 + trueLineCount + (falseLineCount ? falseLineCount + 2 : 0)
}
class If extends Stmt {
    constructor(boolean, trueStmt, falseStmt) {
        super(ifLineCount(trueStmt, falseStmt));
        ...

至於 trueStmtfalseStmt 各有幾行,就不用關心了,因為不會也不應該知道 trueStmtfalseStmt 是什麼類型的陳述,只要信任這些陳述節點,能正確記錄擁有幾行就可以了。

因為 trueStmt 是不包含 if 那行,也不包含 } 那行的,因此上頭的計算中會補上 2,如果有 else,也就是 falseStmt 不是 StmtSequence.EMPTY(即 lineCount 不為 0),再補上 2,這樣就會是 If 陳述容器包含的陳述句行數了。

既然信任陳述句節點,都可以各自記得程式碼的行數,那麼就如 line_parser.js 中看到的:

function isElseLine(tokenableLine) {
    return tokenableLine && tokenableLine.tryTokenables('else')[0];
}

function createIf(tokenableLines, argTokenable) {
    const remains = tokenableLines.slice(1);     
    const trueStmt = LINE_PARSER.parse(remains);
    const trueLineCount = trueStmt.lineCount;

    const i = trueLineCount + 1;
    const falseStmt = isElseLine(remains[i]) ? 
            LINE_PARSER.parse(remains.slice(i + 1)) : 
            StmtSequence.EMPTY;
    const falseLineCount = falseStmt.lineCount;
    
    const linesAfterIfElse = tokenableLines.slice(
        2 + trueLineCount + (falseLineCount ? falseLineCount + 2 : 0)
    );

    return new StmtSequence(
        new If(
            EXPR_PARSER.parse(argTokenable), 
            trueStmt,
            falseStmt
        ),
        LINE_PARSER.parse(linesAfterIfElse),
        tokenableLines[0].lineNumber
    );
}

在能剖析出 trueStmt 節點後,只要透過 lineCount,就能知道 if 用掉了幾行,接著看看有沒有 else,同樣地最後透過 lineCount,知道它用掉了幾行,這樣就能知道接下來要從哪行繼續剖析下去了。