在 Cargo Shuffle 中,你认识了 Factor 的数据栈,以及那些把它重新排列的单词。这个练习在此基础上继续:你要定义的单词不只是把输入搬来搬去,而是用它们进行计算。
!表示行注释开始,从!到行尾的内容 Factor 都会忽略。下面的例子用!既标注代码的作用,也展示它会输出什么。
单词是 Factor 对函数的称呼。调用一个单词会从栈顶弹出一些值,再压回一些值。.弹出栈顶的值并打印它;整数运算单词+、-、*会弹出两个数字,再把结果压入栈:
2 3 + . ! prints 5
8 3 - . ! prints 5 (8 - 3, not 3 - 8)
2 3 * . ! prints 6
另一个相关的单词.s会打印整个数据栈,而且不消耗任何东西。在监听器(Factor 的交互式提示符,你在那里输入代码并看到结果)里,它很适合用来看一段代码在栈上留下了什么。后面的例子有时以.s而不是.结尾,原因就在这里:它们在栈上留下不止一个值,而.s会把它们全部显示出来。
这些运算单词属于math词汇表,所以用到它们的文件需要在USING:那一行里写上math。
每个单词都会用一个形如( inputs -- outputs )的栈效应来记录说明。它是这个单词的约定:该单词从栈顶弹出输入,并在原处留下输出。括号里的名字只是帮你阅读用的。Factor 并不使用它们,它只关心各个值在栈中的顺序。
! + is specified as ( x y -- sum )
! . is specified as ( x -- )
栈顶是右边那个输入。所以8 3 -中3在栈顶,栈效应是( x y -- difference ),结果是8 - 3。
输出中末尾的?是表示“布尔值”的惯例,不过千层面这个练习只用到数字。
下面会用到 Cargo Shuffle 里的一个重排单词:swap ( x y -- y x ),当栈顶的两个值顺序不对、不适合下一个单词时,它会把它们对调。
:开始一个单词定义,接着是栈效应,然后是函数体,最后用;结束。
: square ( x -- x^2 ) dup * ;
4 square . ! => 16
Factor 的编译器会检查函数体是否真的与声明的栈效应相符:如果一个单词声称是( x -- y ),却在栈上留下零个或两个值,就无法通过编译。
CONSTANT:为一个固定的值定义名字。常量本身就是一个单词,调用它会把这个值压入栈:
CONSTANT: pi 3
pi pi * . ! => 9
CONSTANT:属于核心语法,不需要USING:那一行。把常量放在文件顶部,放在任何会用到它们的单词之前。
单词的函数体可以调用任何已经在作用域内的单词,包括你在同一个文件里更早定义的那些:
: double ( x -- 2x ) 2 * ;
: quadruple ( x -- 4x ) double double ;
5 quadruple . ! => 20
练习里的最后一个任务就是这样复用前面的任务的。
单词和常量都使用lowercase-kebab-case:小写字母之间用连字符连接(例如expected-bake-time、preparation-time)。
在本练习中,你将编写一些代码,帮你按照最喜欢的菜谱做出一份美味的千层面。
你有四个任务,全都与烹饪千层面所花的时间有关。
定义常量 expected-bake-time,它应该返回千层面需要在烤箱里烘焙多少分钟。
根据菜谱,千层面需要在烤箱里总共烤 40 分钟。
expected-bake-time .
! => 40
定义 preparation-time 这个单词。它从栈上取出你给千层面加的层数,留下你花在准备上的分钟数,假设每层需要准备 2 分钟。
4 preparation-time .
! => 8
定义 remaining-time 这个单词。它接收千层面已经在烤箱里烤过的分钟数,留下它还需要在里面待多少分钟。
25 remaining-time .
! => 15
定义 total-working-time 这个单词。它从栈上取出两个实参:先是千层面的层数,然后是千层面已经在烤箱里的分钟数;然后留下目前为止烹饪所花的总时间:准备时间加上已经在烤箱里度过的时间。
3 20 total-working-time .
! => 26