對於簡短的函式,堆疊操作就夠用了。但當一個函式有好幾個輸入,而你需要用名稱來指涉它們時,Factor 提供了區域變數,這種具名綁定可以用::(用於函式定義)或[| | ](用於行內匿名函式)來引入。
:: ( inputs -- outputs )把:換成::,輸入名稱就會在函式主體中變成區域變數:
USING: locals ;
:: hypotenuse ( a b -- c )
a a *
b b *
+ sqrt ;
和堆疊搬移的版本比較一下:
! DOCTEST: SKIP (shown for comparison — would re-define hypotenuse)
: hypotenuse ( a b -- c )
[ sq ] bi@ + sqrt ;
兩種寫法都行得通。當某個輸入被引用超過一次,或是運算順序無法自然地和堆疊順序對應時,區域變數就特別好用。
:>綁定中間值在::的主體裡(或是行內[| ... |]匿名函式中),:>會從資料堆疊彈出一個值,並把它綁定到一個新的區域變數:
value :> name ! immutable binding
value :> name! ! mutable binding (name! is its setter)
不可變的形式比較常用,用它為中間值命名,可以讓主體由上往下讀:
USING: locals math ;
:: average-3 ( x y z -- avg )
x y + z + :> sum
sum 3 / ;
帶有!的版本會把這個區域變數宣告成可變;在主體稍後的位置,用value name!(沒有冒號,也沒有:>)就能重新指定它的值:
USING: kernel locals math math.order ;
:: clamped ( x lo hi -- y )
x :> v!
v lo < [ lo v! ] when
v hi > [ hi v! ] when
v ;
可變的區域變數在你之後練習會遇到的疊代函式(例如while)中特別好用。
[let:在任何地方建立作用域:>只能在詞法作用域中使用,而::和[| ... |]會自動建立這種作用域。如果想從一般的:函式或互動式環境中引入區域變數,就把程式碼包在[let ... ]裡:
[let code :> name code :> name ... body ]
每個:> name都會綁定前一個運算式堆疊頂端的值,而這些綁定對整個形式的其餘部分都看得見。
USING: locals math ;
[let 3 :> x 4 :> y x x * y y * + ] . ! => 25
[let是堆疊風格的:定義和具名綁定之間的橋樑。當一個函式中只有一小部分能受益於區域變數時,就很方便。
[| inputs | body ]
在程式區塊裡,[| inputs | body ]會引入主體可以用名稱指涉的區域變數。這在像assoc-map這類高階函式中特別有用,它收到的程式區塊會同時拿到key和value:
USING: assocs locals ;
H{ { "a" 1 } { "b" 2 } }
[| key value | key value neg ] assoc-map .
! => H{ { "a" -1 } { "b" -2 } }
同樣的形式也適用於map-index(位於[sequences][sequences]中),它的程式區塊會收到元素和它的索引。這些名稱讓人清楚看出哪個運算元扮演哪個角色:
USING: locals sequences ;
! Divide each element by its 1-based position:
{ 10 80 270 } [| elt i | elt i 1 + / ] map-index .
! => { 10 40 90 }
匿名函式的堆疊效應取決於輸入清單以及主體產生的結果。
在這些情況下可以使用區域變數:
dup/swap/rot又難以閱讀。assoc-map)。當主體是乾淨的線性流程時,單純的:定義反而更短,也至少一樣好讀。
assoc-map:一次處理一個項目assoc-map(位於assocs中)會把程式區塊套用到關聯式集合的每一個項目上:
assoc-map ( assoc quot: ( key value -- newkey newvalue ) -- newassoc )
用帶有區域變數的匿名函式來寫這個程式區塊,是最乾淨俐落的做法。
cond
當你需要根據f或某個數值來分派時,cond正是合適的形式。因為沒有單一值可以比較,所以不需要用case。
你又回到了廚房,改良來自 Leah 的美味千層麵 的千層麵食譜。這裡的任務要你同時處理多個輸入,而這正是區域變數(英文:locals),也就是 Factor 的具名綁定,派上用場的時候。
定義 cooking-status,從堆疊上取得計時器讀數,並回傳一個狀態字串。
0 → "Lasagna is done."
f(計時器從未設定)→ "You forgot to set the timer."
"Not done, please wait."
12 cooking-status . ! => "Not done, please wait."
0 cooking-status . ! => "Lasagna is done."
f cooking-status . ! => "You forgot to set the timer."
定義 preparation-time,接收一個包含各層名稱的陣列,以及每層平均所需的分鐘數,並回傳總準備時間。
{ "sauce" "noodles" "sauce" "meat" "mozzarella" "noodles" } 3 preparation-time .
! => 18
定義 quantities,接收一個包含各層名稱的陣列,並回傳兩個值:所需的麵條公克數(每層麵條 50 公克)以及所需的醬汁公升數(每層醬汁 1/5公升)。
{ "sauce" "noodles" "sauce" "meat" "mozzarella" "noodles" } quantities .s
! => 100
! => 2/5
你的朋友傳來一份食材清單,其中最後一項就是他們的秘密食材。定義 add-secret-ingredient,接收他們的清單和你的清單,並回傳在尾端加上這個秘密食材後你的清單。
{ "noodles" "sauce" "mozzarella" "kampot pepper" }
{ "noodles" "meat" "sauce" "mozzarella" }
add-secret-ingredient .
! => { "noodles" "meat" "sauce" "mozzarella" "kampot pepper" }
這本食譜書上的食譜可做出兩份。定義 scale-recipe,接收一份食譜(從食材名稱對應到份量的雜湊表)以及目標份數,並回傳依比例調整後的新食譜。
H{ { "noodles" 200 } { "sauce" 1/2 } { "mozzarella" 1 } { "meat" 100 } }
4 scale-recipe .
! => H{ { "noodles" 400 } { "sauce" 1 } { "mozzarella" 2 } { "meat" 200 } }