单词较短时,靠栈来回倒腾就够了。但当某个单词有好几个输入,而你又需要按名字引用它们时,Factor 提供了局部变量,也就是用::(用于单词定义)或[| | ](用于内联 lambda)引入的具名绑定。
:: ( 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 ;
两种写法都有效。当同一个输入被引用不止一次,或者运算顺序跟栈的顺序对不上时,局部变量就派上用场了。
:>绑定中间值在::的函数体里(或者是内联的[| ... |] lambda 里),:>会从数据栈上弹出一个值,并把它绑定到一个新的局部变量上:
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:随处创建作用域:>只能在词法作用域里使用,而::和[| ... |]会自动创建这样的作用域。如果想在普通的:单词里,或者在 listener 里引入局部变量,就把代码包在[let ... ]中:
[let code :> name code :> name ... body ]
每个:> name都会绑定前一个表达式的栈顶值;这些绑定对于该形式中剩下的部分都是可见的。
USING: locals math ;
[let 3 :> x 4 :> y x x * y y * + ] . ! => 25
[let是栈式的:定义与具名绑定之间的桥梁,当单词里只有一小部分能受益于局部变量时,它格外好用。
[| inputs | body ]
在 quotation 里,[| inputs | body ]会引入一些局部变量,函数体可以按名字引用它们。这在assoc-map这类高阶单词中尤其有用,因为它的 quotation 会收到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]里),它的 quotation 会收到元素和它的下标。有了名字,哪个操作数扮演什么角色就一目了然了:
USING: locals sequences ;
! Divide each element by its 1-based position:
{ 10 80 270 } [| elt i | elt i 1 + / ] map-index .
! => { 10 40 90 }
lambda 的栈效应由输入列表以及函数体产生的值决定。
出现下面这些情况时,就用局部变量:
dup/swap/rot来修正又很难读。assoc-map那样)。如果函数体是一段干净、线性的流程,那普通的:定义反而更短,可读性也一点不差。
assoc-map:一次处理一个条目assoc-map(在assocs里)会对关联集合中的每个条目应用一个 quotation:
assoc-map ( assoc quot: ( key value -- newkey newvalue ) -- newassoc )
用局部变量的 lambda 是写这个 quotation 最简洁的方式。
cond
当你需要根据f或某个数值来分派时,cond正是合适的写法,不需要case,因为根本不存在可供比较的单个值。
你又回到了厨房,继续打磨 Leah 的美味千层面 里的千层面食谱。这里的任务要求你同时应付好几个输入,而这正是局部变量,也就是 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 } }