while
陳述根據指定的條件式,判斷是否執行 while
本體,用以進行重複性的演算,例如以下是個求最大公因數的程式:
m = Number.parseInt(input('輸入數字 1'))
n = Number.parseInt(input('輸入數字 2'))
while n != 0 {
r = m % n
m = n
n = r
}
println('GCD: ' + m)
ToyLang 沒有其他語言中有的 do while
語句,必須使用判斷式及 break
來達成。例如判斷輸入為奇數或偶數,直到使用者回答 No 為止:
while true {
number = Number.parseInt(input('輸入數字:'))
println('輸入數為' + ('奇數' if number % 2 else '偶數'))
if input('繼續?(Yes/No)') == 'No' {
break
}
}
底下是個很無聊的遊戲,看誰可以最久不撞到這個數字 5:
import '/lib/math'
while true {
number = Number.parseInt(math.random() * 10)
println(number)
if number == 5 {
break
}
}
println('I hit 5....Orz')
ToyLang 有模組管理機制,math
模組是標準模組之一,其中包含了 random
函式,會隨機產生 0.0 到小於 1.0 的值,乘上 10 再裁掉小數部份,表示產生 0 到 9 的數。
如果會建立 if
陳述相對應的語法節點,while
相對應的語法節點,基本上不成問題:
class While {
constructor(cond, stmt) {
this.cond = cond;
this.stmt = stmt;
}
evaluate(context) {
let ctx = context;
while(true) {
if(this.cond.evaluate(context).value) {
ctx = this.stmt.evaluate(context);
}
else {
break;
}
}
return ctx;
}
}
也就是每次對 while
的條件式節點求值,結果會是 true
,就一直執行 while
本體的陳述句節點,不過,還得考慮 break
,它會有一個相對應的 Break
節點:
const Break = {
lineCount : 1,
evaluate(context) {
return context.broken();
}
};
也就是執行 break
,相對應的 Break
會在環境物件中做個註記,表示執行了 break
,只要環境物件中這個註記還存在,while
本體中的之後每個陳述節點都不會執行,而 While
節點:
class While {
constructor(cond, stmt) {
this.cond = cond;
this.stmt = stmt;
}
evaluate(context) {
let ctx = context;
while(true) {
if(this.cond.evaluate(context).value) {
ctx = this.stmt.evaluate(context);
if(ctx.isBroken()) {
return ctx.fixBroken();
}
}
else {
break;
}
}
return ctx;
}
}
break
會中斷目前的 while
陳述,因此本體的陳述節點執行過後,檢查 ctx.isBroken()
看看環境物件有無註記,有的話就 return
,然而在 return
之前,必須先清楚環境物件中關於 break
的註記,這就是 ctx.fixBroken()
做的事情,否則在 while
陳述後若還有其他陳述,也會無法執行。
檢查每個陳述句執行過後,環境物件是否有被註記,實際上應該實作在 StmtSequence
之中,不過,程式中的 StmtSequence
何其多,每執行一次陳述句,就得檢查一次環境物件,著實沒有效率,因而實際上,我並沒有真的做檢查的動作,而是使用回呼函式。
每個環境物件都有個 notBroken
函式,上一個陳述句若傳回 ctx
,要執行下個陳述句 stmt
的話,程式碼上會是:
ctx.notBroken(c => stmt.evaluate(c));
notBroken
平常參考的函式會是:
function call(f) {
return f(this);
}
也就是方才的 c
在正常執行流程下,就是 ctx
,萬一有個 break
執行了,對應的節點會執行環境物件的 broken
方法,這時環境物件的 notBroken
會被代換為:
function self(f) {
return this;
}
也就是此時,會忽略掉傳入的 f
,因此等同於略過回呼中指定的 stmt
之估值,這樣就不用每次都檢查有沒有被註解 break
了;如果呼叫了環境物件的 fixBroken
方法,就會將 notBroken
參考的函式,恢復為方才的 call
函式。
為什麼取名為 notBroken
,因為可讀性,也就是 ctx.notBroken(c => stmt.evaluate(c))
代表的是,在沒有 break
下執行回呼函式。
方才談到的實作,都可以在 context.js 與 statement.js 中看到,實際的流程會再複雜一些,因為還處理了 return
與 throw
的情況。
至於為什麼要採用在環境物件上註記的方式呢?記得之前談過陳述句與運算式的差別嗎?
若是陳述句,通常會對環境物件造成狀態變更,因此陳述句節點,必須傳回環境物件,如此下個陳述節點,才能在最新狀態的環境物件基礎下,繼續執行程式;如果是運算式節點,通常傳回自身,如此下個運算節點,才可以從傳回的物件上取得內含值,像是 xxx.value
這樣的動作。
運算式通常被包含在一個陳述句中,或者之後會看到,被包裝為在一個陳述節點之中,簡而言之,無論是哪個,最新的環境物件都必須被不斷傳遞下去;如果不在環境物件上註記,直接傳回一個代表執行過 break
的物件,那麼上一個陳述句節點執行過後的環境物件狀態,就勢必被中斷,程式的狀態就無法不斷地傳遞下去了。
既然談到了 While
節點,可以看一下 statement.js 中 While
的實作:
class While extends Stmt {
constructor(boolean, stmt) {
super(stmt.lineCount + 2);
this.boolean = boolean;
this.stmt = stmt;
}
evaluate(context) {
let ctx = context;
while(true) {
const maybeContext = this.boolean.evaluate(ctx);
if(maybeContext.thrownNode) {
return maybeContext;
}
if(maybeContext.value) {
ctx = this.stmt.evaluate(ctx);
if(ctx.thrownNode || ctx.returnedValue) {
return ctx;
}
if(ctx.isBroken()) {
return ctx.fixBroken();
}
} else { break; }
}
return ctx;
/*
To avoid 'Maximum call stack size exceeded', the above code is the only place which uses 'while'.
The corresponding code with the functional style is shown below.
*/
// const maybeContext = this.boolean.evaluate(context);
// return maybeContext.notThrown(v => {
// if(v.value) {
// const ctx = this.stmt.evaluate(context);
// return ctx.either(
// leftContext => leftContext,
// rightContext => rightContext.notReturn(
// c => c.isBroken() ? c.fixBroken() : this.evaluate(c)
// )
// );
// }
// return context;
// });
}
}
嗯?那段註解是什麼?你可以試著找找看 ToyLang 的 .js 實作原始碼,看看其他地方有沒有出現 while
,實際上,這是唯一使用到 while
的地方喔!另外,在 .js 實作原始碼中,絕大多數的地方在宣告變數時是使用 const
,只有在少數地方使用到 let
。
在實作 ToyLang 時,我特意在大多數的地方採用函數式典範,這有很大的好處,特別是對函式流程的抽取、模組與類別職責的分配,以及模組與物件之間的獨立性等,我一直都有談到,對於語言實作來說,元件的獨立性很重要,若以函數式典範作為出發點,你就得一開始就考慮這些要素。
也就因此,具有重複性的邏輯,很多都被 filter
、map
、reduce
這種基本模式的實作品給解決掉了,filter
、map
、reduce
做不到的,就採用遞迴解決,結果就是每個函式流程都很精簡,每個模組也不巨大。
然而,這在實作 while
就會有問題,因為 JavaScript 並不支援遞迴的展開,也就因此,會受到遞迴深度的限制,上面程式碼註解的部份,就是實作 while
時對應的遞迴版本,實作它是有價值的,然而,因為受到遞迴深度的限制,while
本體變成差不多只能執行 2000 次左右了。
雖然是 ToyLang,然而,如果 while
只能執行 2000 次左右,那就連 Toy 都稱不上了,對吧!?這也就令此處,成了唯一有使用到 while
實作的地方了!
話說,一下子又是 ToyLang 的 while
,一下子又是 JavaScript 的 while
,你知道我正在談哪個 while
嗎?沒辦法!選的關鍵字雷同,經常會搞不清楚嘞!
之後在實作類別與實例時,我也是常得保持腦袋清楚,不然,很容易搞不清楚,我現在是想要 JavaScript 的某個實例,還是 ToyLang 的某個實例啊…XD