if 陳述
December 18, 2021if…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
節點不用去管 trueStmt
、falseStmt
是什麼,因為全部的陳述句節點,都會有相同的介面,只要判斷要執行哪個就好了,然而,只有 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
,甚至是 while
、switch
,甚至是區域函式定義等,它們會以無限種方式組成,你想要的資料結構,是不可能考慮無限種可能的。
在土炮 Toy Lang 的過程,我曾經採取的尋找對稱的 {
與 }
,然而,這方式最後是失敗的,原因之一是每增加一種陳述句,尋找對稱 {
與 }
就要加入新的判斷,逐漸使得演算執來越複雜,原因之二就是上述的,演算本身已經沒有獨立性,最後也無法涵蓋各種可能的組合情況。
為什麼不讓各自的陳述句,記得各自擁有多少行呢?
class Stmt {
constructor(lineCount) {
this.lineCount = lineCount;
}
}
那麼,StmtSequence.EMPTY
算是一行嗎?不!在 Toy Lang 的設計中,StmtSequence.EMPTY
只是用來組合 StmtSequence
時使用,並不是直接代表 }
,它還會用來代表上一個區塊結束,或者程式碼已結束,因此我讓 StmtSequence.EMPTY
的 lineCount
為 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
這種陳述句容器,就是將 trueStmt
、falseStmt
加總起來:
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));
...
至於 trueStmt
、falseStmt
各有幾行,就不用關心了,因為不會也不應該知道 trueStmt
、falseStmt
是什麼類型的陳述,只要信任這些陳述節點,能正確記錄擁有幾行就可以了。
因為 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
,知道它用掉了幾行,這樣就能知道接下來要從哪行繼續剖析下去了。