| 原作者 | Robert Morris(英语:Robert Morris (cryptographer)) (于AT&T贝尔实验室) Lorinda Cherry(英语:Lorinda Cherry) |
|---|---|
| 開發者 | 各种开源和商业开发者 |
| 编程语言 | B,C[1] |
| 操作系统 | Unix,类Unix,Plan 9 |
| 平台 | 跨平台 |
| 类型 | 命令 |
dc(deskcalculator:桌面计算器)是采用逆波兰表示法的跨平台计算器,它支持任意精度算术[2]。它是Robert Morris(英语:Robert Morris (cryptographer))在贝尔实验室工作期间书写的[3],作为最老的Unix实用工具,先于C语言的发明。像那个年代的其他实用工具一样,它有着一组强力的特征和简洁的语法[4][5]。传统上,采用中缀表示法的bc计算器程序是在dc之上实现的。
dc是幸存的最老的Unix语言[3]。在贝尔实验室收到第一台PDP-11的时候,用B语言写成的dc是在这个新机器上运行的第一个语言,甚至在汇编器之前[6]。
在dc中要做4和5的乘法:
$dc4 5 *p20q
这可转译为“把4和5压入栈顶,通过乘法算符,从栈中弹出两个元素,将二者相乘并把结果压回栈顶”。接着使用p命令打印栈顶的元素。使用q命令退出此次调用的dc实例。注意数值相互间必须以空白分隔,但某些算符可以不必如此。还可以用如下命令得到这个结果:
$dc-e'4 5 * p'20$echo"4 5 * p"|dc20$dc-4 5 *pq20$cat<<EOF>cal.dc4 5 *pEOF$dccal.dc20
使用命令k来变更算术精度,它设置算术运算的小数位数。缺省精度是0,例如:
$dc-e'10946 6765 / p'1
通过使用命令k调整精度,可以产生任意数目的小数数位,例如:
$dc-e'7k 10946 6765 / p'1.6180339
dc有科学计算器的基本运算功能,比如求黄金分割率的值:
$dc-e'7k 5 v 1 + 2 / p'1.6180339
dc将输入数值的前导_识别为负号,命令^计算幂,命令v计算平方根。
d命令用于复制栈顶元素。r命令用于对栈顶和仅次栈顶的两个元素进行对换。z命令用于压入当前栈深度,即执行z命令前栈中元素的数目。
使用?命令,从stdin读取一行并执行它。这允许从宏中向用户要求输入,故而此输入必须是语法上正确的,并且这有潜在的安全问题,因为dc的!命令可以执行任意系统命令。
前面提及过,p命令打印栈顶元素,带有随后的一个换行。n命令弹出栈顶元素并输出它,没有尾随换行。f命令打印整个栈,从栈顶到栈底并且一项一行。
dc还支持控制输入和输出的底数。o命令设置输出底数,输出底数必须大于等于2。i命令弹出栈顶元素并将它用作输入底数,输入底数必须在2和16之间,十六进制数字必须大写以避免和dc命令冲突。要记住输入底数将影响对后面的所有数值的分析,所以通常建议在设置输入底数之前先设置输出底数。例如将二进制转换成十六进制:
$echo'16o2i 10011010101111001101111011110000 p'|dc9ABCDEF0
要读取设置的这些数值,K、I和O命令将压入当前精度、输入基数和输出基数到栈顶。
除了上述的基本算术和栈操作,dc包括了对宏、条件和存储结果用于以后检索的支持。
寄存器在dc中是有着单一字符名字的存贮位置,它可以分别通过命令s和l来存储和检索,它是宏和条件的底层机制:sc弹出栈顶元素并将它存储入寄存器c,而lc将寄存器c的值压入栈顶。例如:
3 sc 4 lc * p
寄存器还被当作次要栈,可以使用S和L命令在它们和主要栈之间压入和弹出数值。存储栈顶元素到寄存器中并把这个元素留在栈顶,需要联合使用ds命令。
字符串是包围在[和]之中的字符,可以被压入栈顶和存入寄存器。使用x命令从栈顶弹出字符串并执行它,使用P命令从栈顶弹出并打印字符串,无尾随换行。a命令可以把数值的低位字节转换成ASCII字符,或者在栈顶是字符串时把它替换为这个字符串的第一个字符。此外没有方法去建造字符串或进行字符串操纵。
#字符开始一个注释直到此行结束。
通过允许寄存器和栈项目像数值一样存储字符串,从而实现了宏。一个字符串可以被打印,也可以被执行,就是说作为dc命令的序列而传递。例如可以把一个宏“加1并接着乘以2”存储到一个寄存器M中:
[1+ 2*]sM
下面的命令将一个数值3压入栈顶,使用x命令执行存储在寄存器M中的宏,并打印留在栈顶的结果:
3 lMx p
最后提供了有条件执行宏的机制。命令=M将从栈顶弹出两个值,如果二者相等,则执行存储在寄存器M中的宏。如下命令序列将在原栈顶元素等于5的条件下打印字符串equal。
[[equal]p]sM d5=M
这里使用了d命令保留原栈顶元素。其他条件有>、!>、<、!<、!=,如果栈顶元素分别大于、不大于(小于等于)、小于、不小于(大于等于)、不等于仅次于栈顶的元素,则执行指定的宏。注意不同于Forth、PostScript和Factor,在不等式比较中的运算元的次序同在算术中的次序相反,5 3 - 等价于中缀表示法的5-3,然而5 3< R在3<5时运行寄存器R的内容。下面是有条件执行宏的示例:
$echo5|dc-e'? [[equal]p]sM d5=M'equal$echo4|dc-e'? [[equal]p]sM d5=M'$
这里的命令在栈顶元素等于5之时有相应的输出,在它不等于5之时没有输出。
如果要实现一般编程语言中表达有两个分支控制流程的条件运算符?:或条件语句,则需要两个宏构造,例如:
$echo5|dc-e'? [[equal]p q]sM [d5=M [not equal]p]x'equal$echo4|dc-e'? [[equal]p q]sM [d5=M [not equal]p]x'not equal
这里的命令在栈顶元素等于5和不等于5之时都有相应的输出。
q命令退出2层宏,如果宏少于2层则退出dc。Q命令从栈顶弹出一个值作为退出宏的层数,比如2Q命令退出2层宏,它永不导致退出dc。
通过定义进行有条件的递归调用的宏,可以实现递归过程和迭代运算。
# F(x):# x > 1 ? x * F(x-1) : x# F()
本文中的伪代码尽量采用条件运算符?:而少用条件语句。这里的F(x):是过程定义,过程F消费在栈顶的命名为x的一个值;这里的F(x-1)是过程调用,在调用过程F之前先将x-1的结果值压入栈顶;这里的F()也是过程调用,在调用过程F之前不需要向栈顶压入一个值。
这个过程在x > 0时有效,它可直接转写为互递归形式:
# F(x):# x > 1 ? G(x) : x# G(x):# x * F(x-1)# F()
它可实现为:
[d 1- lFx *]sG [d1<G]dsFx
计算阶乘还可以采用这个条件表达式的等价形式:
# F(x):# x <= 1 ? x : x * F(x-1)# F()
它可实现为:
[q]sQ [d1!<Q d 1- lFx *]dsFx
这个递归过程可以进一步采用尾调用写为:
# F(x):# x# x > 1 ? G(x) : x# G(x):# F(x-1)# H(x, acc):# x := x * acc# z() > 1 ? H() : x# H(F())
这里的伪代码中的H(x, acc):是过程定义,过程H消费在栈顶的命名为acc的一个值,和紧邻其下的命名为x的另一个值;这里的H()是过程调用,在调用过程H之前不需要向栈顶压入一个值。它可实现为:
[1- lFx]sG [d d1<G]dsFx [* z1<H]dsHx
这里的运算由两个步骤串接而成,首先将递减的整数压入堆栈形成整数集上偏序关系的区间,然后在这个数列上进行初始值为的应用乘法运算的右侧归约,最终得出一个结果值。这里的命令z,将当前的堆栈深度即堆栈中元素数目压入栈顶。还可以进一步增加针对初始的栈顶元素x的x ≤ 0情况的预处理,即执行x := x-x+1来实现x := 1。下面是其执行示例:
$echo0|dc-e'? [d-1+]sAd0!<A [1- lFx]sG [d d1<G]dsFx [* z1<H]dsHx p'1$echo9|dc-e'? [d-1+]sAd0!<A [1- lFx]sG [d d1<G]dsFx [* z1<H]dsHx p'362880
# n := x# i := 1# F(x):# x := x * i # p(x)# i := i + 1# i <= n ? F() : x# F(1)
这里n := x中的x指称初始的栈顶元素。这里的迭代过程F(x)不同于一般的递归过程之处,是在调用自身之时仍采用当前栈顶元素为参数而不向堆栈压入新元素。这里所迭代的是需要初始化的堆栈中的自动变量x,它是,其初始值1是;i既是迭代的递增计数器,也是迭代二元运算(英语:Iterated binary operation)的运算元,其递增形成整数集上偏序关系的区间,同步地在这个数列上进行输出中间值的初始值为的应用乘法运算的左侧归约,逐项输出中间结果形成数列,这里的x所起到的作用也被称为累加器(accumulator),其含义为“累计”或“累积”而不必然采用加法或乘法。它可实现为:
sn 1si 1 [lid1+si *p liln!<F]dsFx
下面是其执行示例:
$echo6|dc-e'? sn 1si 1 [lid1+si *p liln!<F]dsFx'12624120720
可以将它改为计算单个的阶乘:
$echo0|dc-e'? sn 1si 1 [lid1+si * liln!<F]dsFx p'1$echo9|dc-e'? sn 1si 1 [lid1+si * liln!<F]dsFx p'362880
# F(x):# x > 1 ? F(x-1) + F(x-2) : x# F()
这个过程在x ≥ 0时有效,它可直接转写为互递归形式:
# F(x):# x > 1 ? G(x) : x# G(x):# F(x-1) + F(x-2)# F()
它可实现为:
[d 2- lFx r 1- lFx +]sG [d1<G]dsFx
这里的r命令反转(交换)栈顶元素和仅次于栈顶的元素二者的次序,此外dc还有循环移位运算(英语:Circular shift)R命令,其参数为正数时是循环左移一位,参数为负数时是循环右移一位,参数的绝对值是参与循环移位的堆栈中含栈顶的元素数目,比如后文中用到_3R命令会将堆栈顶部的三个元素a b c转变为c a b。
下面是其执行示例,并展示了通过给它加打印断点来观察其递归调用的规模:
$echo0|dc-e'? [d 2- lFx r 1- lFx +]sG [d1<G]dsFx p'0$echo20|dc-e'? [d 2- lFx r 1- lFx +]sG [d1<G]dsFx p'6765$echo20|dc-e'? [p d 2- lFx r 1- lFx +]sG [d1<G]dsFx'|wc-l10945$echo21|dc-e'? [d 2- lFx r 1- lFx +]sG [d1<G]dsFx p'10946
这个过程可以进一步采用记忆化而写为:
# i := 1# F(x):# x > 1 ? M(x) : x# M(x):# if x <= i# a[x]# else # temp := G(x)# i := i + 1# a[i] := temp # temp# G(x):# F(x-1) + F(x-2)# F()
基于数组的保存命令:和访问命令;,它可实现为:
1si [d 2- lFx r 1- lFx +]sG [;a q]sN [dli!<N lGx dli1+dsi:a]sM [d1<M]dsFx
在递归定义中,正在计算并要记忆其结果的是,即将它的结果存储在这里的a[n]中,其下标为n,而当前已存储过的最大下标是n-1,将要存储的下标是已经存储的最大下标的增加1。这里的i是数组a的已存储过的最大下标,它的初始值是1,即假定a[0]和a[1]已经存储了数值。这里的F(x)在被遇到0和1这两个参数之时,直接在代码中返回数值,而不会用它们作为下标去访问数组。
这个过程可以更一般化的写为:
# a[0] := 0# a[1] := 1# i := 1# F(x):# if x <= i# a[x]# else# S(x, b) # temp := G(x)# x := L(b)# i := x# a[x] := temp # temp# G(x):# F(x-1) + F(x-2)# F()
这里的伪代码中的S(x, b)表示复制主栈的栈顶元素x并把它压入栈b的栈顶,L(b)表示弹出栈b的栈顶元素并把它压入主栈的栈顶。基于寄存器关联栈的保存命令S和装载命令L,它可实现为:
0 0:a 1 1:a 1si [d 2- lFx r 1- lFx +]sG [;a q]sN [dli!<N dSb lGxd Lbdsi:a]dsFx
在递归定义的递归调用中,随着的索引n的递减,而递归下降至被计算出来并保存的第一项,然后沿着调用链逐级返回。在递归的每个步骤中都要计算并保存,它所需要的和这两项之中:
下面是其执行示例,展示了通过给它加打印断点来观察其递归调用的具体步骤,其中连续高亮标示出某个过程从开始被调用直到它结束返回:
$echo8|dc-e'? 0 0:a 1 1:a 1si [d 2- lFx r 1- lFx +]sG [;a q]sN [[Level]nzn[ F]np dli!<N dSb lGxd Lbdsi:a]dsFx'Level1 F8Level2 F6Level3 F4Level4 F2Level5 F0Level5 F1Level4 F3Level5 F1Level5 F2Level3 F5Level4 F3Level4 F4Level2 F7Level3 F5Level3 F6$echo8|dc-e'? 0 0:a 1 1:a 1si [d 1- lFx r 2- lFx +]sG [;a q]sN [[Level]nzn[ F]np dli!<N dSb lGxd Lbdsi:a]dsFx'Level1 F8Level2 F7Level3 F6Level4 F5Level5 F4Level6 F3Level7 F2Level8 F1Level8 F0Level7 F1Level6 F2Level5 F3Level4 F4Level3 F5Level2 F6
递归过程的返回值之间形成了递推关系,每个的值被用到一次或两次,首次是从递归调用直接返回,再次是对其已保存的值进行访问,每个步骤要么访问两项并保存两项,要么访问一项并保存一项。故而它可以进一步写为:
# i := 1# F(x):# x > 1 ? M(x) : x# M(x):# if x <= i# a[x%2]# else # temp := G(x)# i := i + 1# a[i%2] := temp # temp# G(x):# F(x-1) + F(x-2)# F()
它可实现为:
1si [d 2- lFx r 1- lFx +]sG [2%;a q]sN [dli!<N lGx dli1+dsi2%:a]sM [d1<M]dsFx
在递归定义的上述两种求值次序中,首先进行递归调用的这种形式,其记忆化实现从一开始就持续进行递归调用直到首次递归返回,然后就逐级递归返回而不再进行递归调用,故而它可以进一步写为:
# a := 0# F(x):# x > 1 ? G(x) : x# G(x):# temp := F(x-1)# prev := a # a := temp# temp + prev# F()
这里的a被初始化为的值0,x在步入递归调用之后,除了在被递减至的值1之时作为调用结果来返回之外,没有参与到后续的加法运算之中,而主要起到了递减计数器的作用。它可实现为:
0sa [1- lFx la r dsa +]sG [d1<G]dsFx
下面的例子采用迭代法打印出斐波那契数列的不含第0项的前n项:
# i := x# a := 1# F(x):# prev := a# a := x# x := x + prev# p(x)# i := i - 1# i > 0 ? F() : x# i > 0 ? F(0) : 0
这里i := x中的x指称初始的栈顶元素。这里的迭代过程F(x)不同于一般的递归过程之处,是在调用自身之时仍采用当前栈顶元素为参数而不向堆栈压入新元素。针对递推关系,它总共进行n次迭代运算得出到。这里的i是进行迭代的递减计数器,所迭代的是需要初始化的两个变量x和a:在堆栈中的自动变量x,先是被迭代的,再是迭代出来的,其初始值0是,它所起到的作用也被称为累加器;作为全局变量的a,以所得的1作为初始值,在首次迭代之后它是始于的。它可实现为:
si 1sa [lardsa +p li1-si li0<F]sF 0 li0<F
下面是其执行示例:
$echo6|dc-e'? si 1sa [lardsa +p li1-si li0<F]sF 0 li0<F'112358
可以将它改为计算单个的斐波那契数:
$echo0|dc-e'? si 1sa [lardsa + li1-si li0<F]sF 0 li0<F p'0$echo9|dc-e'? si 1sa [lardsa + li1-si li0<F]sF 0 li0<F p'34
下面的例子通过男人抑或男孩测试展示dc的数组能支持递归运算的程度,这个测试用Python可以写为:
importsyssys.setrecursionlimit(2**10+9)# 这里设置的递归限制在实测时刚好够用于k=10defA(k,x1,x2,x3,x4,x5):defB():nonlocalkk-=1returnA(k,B,x1,x2,x3,x4)returnx4()+x5()ifk<=0elseB()print(A(10,lambda:1,lambda:-1,lambda:-1,lambda:1,lambda:0))
通过手工设立并操纵简易调用栈、非局部引用(英语:Non-local variable)和头等函数,这个测试可以用dc实现为:
# M[0] := S := T := K[0] := 0# P(): # S := T + 1# n := z()# T := T + n + 1# M := R(n, n)# R(x, n):# M[S+n] := x# n := n - 1# n >= 0 ? R(n) : n# Q():# T := T - M[T] - 1# S := T - M[T]# U(x):# I := I + 1# W[I] := x# K[I] := S# V():# I := I - 1# W(i):# W[i](i)0 0:M 0sS 0sT 0 0:K[lT1+sS z d1+lT+sT dlRxsM]sP [d_3RlS+:M 1- d0!>R]sR[lTd;M-1-sT lTd;M-sS]sQ[lI1+sI lI:W lSlI:K]sU[lI1-sI]sV [d;Wx]sW# A(k, x1, x2, x3, x4, x5):# P(); F(); Q()# F():# M[S] <= 0 ? G() : U('B'), B(I), V()# G():# S(W(M[S+5]), O)# W(M[S+4]) + L(O)# B(x):# L := K[x]# J := x# M[L] := M[L] - 1# A(M[L], J, M[L+1], M[L+2], M[L+3], M[L+4])[lPx lFx lQx]sA [lS;M0!<G [lBx]lUx lIlBx lVx]sF[lS5+;M lWx SO lS4+;M lWx LO + q]sG[d;KsL sJ lL;M1-lL:M lL;M lJ lL1+;M lL2+;M lL3+;M lL4+;M lAx]sB# I := 4# W[0] := '(x):1'; W[1] := '(x):-1'; W[2] := '(x):-1';# W[3] := '(x):1'; W[4] := '(x):0'# p(A(?(), 0, 1, 2, 3, 4))4sI [sM1]0:W [sM_1]1:W [sM_1]2:W [sM1]3:W [sM0]4:W? 0 1 2 3 4 lAx p对于k = 10,这个测试中函数A()执行了722次,这里它在传值调用形式参数小于等于零的情况下,先求值第5个传名调用形式参数再求值第4个传名调用形式参数,其在调用栈中的栈帧数量的最大值即递归深度是323层;而为函数B()的创建了416个闭包,其堆叠数量的最大值是258层;并为常量函数创建了5个闭包。
将上述代码保存入manorboy.dc文件中,下面是针对k为0到10它的执行示例:
$foriin$(seq010);doecho$i|dcmanorboy.dc;done10-20101-1-10-30-67
可以将闭包也存储在调用栈之中:
# M[0] := S := T := K[0] := N[0] := I := 0# P(): # S := T + 1# n := z()# T := T + n + 1# M := R(n, n)# I := I + 1# K[I] := S# N[I] := T# R(x, n):# M[S+n] := x# n := n - 1# n >= 0 ? R(n) : n# Q():# T := T - M[T] - 1# S := T - M[T]# I := I - 1# W(i):# M[N[i]-1](i)0 0:M 0sS 0sT 0 0:K 0 0:N 0sI[lT1+sS z d1+lT+sT dlRxsM lI1+sI lSlI:K lTlI:N]sP [d_3RlS+:M 1- d0!>R]sR[lTd;M-1-sT lTd;M-sS lI1-sI]sQ[d;N1-;Mx]sW# A(k, x1, x2, x3, x4, x5):# P('B'); F(); Q()# F():# M[S] <= 0 ? G() : B(I)# G():# S(W(M[S+5]), O)# W(M[S+4]) + L(O)# B(x):# L := K[x]# J := x# M[L] := M[L] - 1# A(M[L], J, M[L+1], M[L+2], M[L+3], M[L+4])[[lBx]lPx lFx lQx]sA [lS;M0!<G lIlBx]sF[lS5+;M lWx SO lS4+;M lWx LO + q]sG[d;KsL sJ lL;M1-lL:M lL;M lJ lL1+;M lL2+;M lL3+;M lL4+;M lAx]sB# P('(x):1'); P('(x):-1'); P('(x):-1'); P('(x):1'); P('(x):0')# p(A(?(), 1, 2, 3, 4, 5))[sM1]lPx [sM_1]lPx [sM_1]lPx [sM1]lPx [sM0]lPx? 1 2 3 4 5 lAx p这里在k = 10的情况下,调用栈的递归深度为323 + 5 = 328层,如果在函数A()中先求值第4个传名调用形式参数再求值第5个传名调用形式参数,则递归深度为512 + 5 = 517层。
dc(1): an arbitrary precision calculator – Linux用户命令(User Commands)手册页