轨道
/
Factor
Factor
/
练习
/
千层面大师
千层面大师

千层面大师

学习练习

简介

单词较短时,靠栈来回倒腾就够了。但当某个单词有好几个输入,而你又需要按名字引用它们时,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是栈式的:定义与具名绑定之间的桥梁,当单词里只有一小部分能受益于局部变量时,它格外好用。

lambda:[| 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来修正又很难读。
  • 你正处在一个内联 lambda 里,它被传给一个会一次交给你好几个值的高阶单词(就像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 的具名绑定,发挥作用的时候。

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,全部免费。