學習軌道
/
Factor
Factor
/
練習
/
千層麵大師
千層麵大師

千層麵大師

學習練習

簡介

對於簡短的函式,堆疊操作就夠用了。但當一個函式有好幾個輸入,而你需要用名稱來指涉它們時,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 的具名綁定,派上用場的時候。

1. 判斷烹飪狀態

定義 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."

2. 估算準備時間

定義 preparation-time,接收一個包含各層名稱的陣列,以及每層平均所需的分鐘數,並回傳總準備時間。

{ "sauce" "noodles" "sauce" "meat" "mozzarella" "noodles" } 3 preparation-time .
! => 18

3. 計算所需的麵條和醬汁

定義 quantities,接收一個包含各層名稱的陣列,並回傳兩個值:所需的麵條公克數(每層麵條 50 公克)以及所需的醬汁公升數(每層醬汁 1/5公升)。

{ "sauce" "noodles" "sauce" "meat" "mozzarella" "noodles" } quantities .s
! => 100
! => 2/5

4. 加入秘密食材

你的朋友傳來一份食材清單,其中最後一項就是他們的秘密食材。定義 add-secret-ingredient,接收他們的清單和你的清單,並回傳在尾端加上這個秘密食材後你的清單。

{ "noodles" "sauce" "mozzarella" "kampot pepper" }
{ "noodles" "meat" "sauce" "mozzarella" }
add-secret-ingredient .
! => { "noodles" "meat" "sauce" "mozzarella" "kampot pepper" }

5. 調整食譜份量

這本食譜書上的食譜可做出兩份。定義 scale-recipe,接收一份食譜(從食材名稱對應到份量的雜湊表)以及目標份數,並回傳依比例調整後的新食譜。

H{ { "noodles" 200 } { "sauce" 1/2 } { "mozzarella" 1 } { "meat" 100 } }
4 scale-recipe .
! => H{ { "noodles" 400 } { "sauce" 1 } { "mozzarella" 2 } { "meat" 200 } }
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Factor Exercism

準備好開始 千層麵大師 了嗎?

註冊 Exercism,透過 47 個概念163 個練習 和真人引導來學習並精通 Factor,全部免費。