Dc (程序)

dc（desk calculator：桌面計算器）是採用逆波蘭表示法的跨平臺計算器，它支持任意精度算術。它是Robert Morris在貝爾實驗室工作期間書寫的，作為最老的Unix實用工具，先於C語言的發明。像那個年代的其他實用工具一樣，它有著一組強力的特徵和簡潔的語法。傳統上，採用中綴表示法的bc計算器程序是在dc之上實現的。

**歷史**
dc是倖存的最老的Unix語言。在貝爾實驗室收到第一臺PDP-11的時候，用B語言寫成的dc是在這個新機器上運行的第一個語言，甚至在彙編器之前。

**基本運算**
在dc中要做4和5的乘法：
$ dc
4 5 p
20
q
這可轉譯為“把4和5壓入棧頂，通過乘法算符，從棧中彈出兩個元素，將二者相乘並把結果壓回棧頂”。接著使用p命令打印棧頂的元素。使用q命令退出此次調用的dc實例。注意數值相互間必須以空白分隔，但某些算符可以不必如此。
還可以用如下命令得到這個結果：
$ dc -e '4 5 * p'
20
$ echo "4 5 * p" | dc
20
$ dc -
4 5 *pq
20
$ cat  cal.dc
4 5 p 
EOF
$ dc cal.dc
20
使用命令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' | dc
9ABCDEF0
要讀取設置的這些數值，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。
equalpsM d5=M
這裡使用了d命令保留原棧頂元素。其他條件有>、!>、、!、!=，如果棧頂元素分別大於、不大於（小於等於）、小於、不小於（大於等於）、不等於僅次於棧頂的元素，則執行指定的宏。注意不同於Forth、PostScript和Factor，在不等式比較中的運算元的次序同在算術中的次序相反，5 3 - 等價於中綴表示法的5-3，然而code|5 335時運行寄存器R的內容。下面是有條件執行宏的示例：
$ echo 5 | dc -e '? equalpsM d5=M'
equal
$ echo 4 | dc -e '? equalpsM d5=M'
$
這裡的命令在棧頂元素等於5之時有相應的輸出，在它不等於5之時沒有輸出。
如果要實現一般編程語言中表達有兩個分支控制流程的條件運算符?:或條件語句，則需要兩個宏構造，例如：
$ echo 5 | dc -e '? equalp qsM d5=M not equalpx'
equal
$ echo 4 | dc -e '? equalp qsM d5=M not equalpx'
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
計算階乘還可以採用這個條件表達式的等價形式：
# F(x):
#   x 
它可實現為：
qsQ d1!
這個遞歸過程可以進一步採用尾調用寫為：
# 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- lFxsG d d1
這裡的運算由兩個步驟串接而成，首先將遞減的整數壓入堆棧形成整數集上偏序關係的區間，然後在這個數列上進行初始值為的應用乘法運算的右側歸約，最終得出一個結果值。這裡的命令z，將當前的堆棧深度即堆棧中元素數目壓入棧頂。還可以進一步增加針對初始的棧頂元素x的x ≤ 0情況的預處理，即執行x := x-x+1來實現x := 1。下面是其執行示例：
$ echo 0 | dc -e '? d-1+sAd0!
下面的例子採用迭代法打印出階乘的前n項的數列：
# n := x
# i := 1
# F(x):
#   x := x * i 
#   p(x)
#   i := i + 1
#   i 
這裡n := x中的x指稱初始的棧頂元素。這裡的迭代過程F(x)不同於一般的遞歸過程之處，是在調用自身之時仍採用當前棧頂元素為參數而不向堆棧壓入新元素。這裡所迭代的是需要初始化的堆棧中的自動變量x，它是，其初始值1是；i既是迭代的遞增計數器，也是Iterated binary operation的運算元，其遞增形成整數集上偏序關係的區間，同步地在這個數列上進行輸出中間值的初始值為的應用乘法運算的左側歸約，逐項輸出中間結果形成數列，這裡的x所起到的作用也被稱為累加器（accumulator），其含義為“累計”或“累積”而不必然採用加法或乘法。它可實現為：
sn 1si 1 lid1+si *p liln!
下面是其執行示例： 
$ echo 6 | dc -e '? sn 1si 1 lid1+si *p liln!
可以將它改為計算單個的階乘：
$ echo 0 | dc -e '? sn 1si 1 lid1+si * liln!

**斐波那契數**
下面是對棧頂元素計算斐波那契數的遞歸過程：
# 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
這裡的r命令反轉（交換）棧頂元素和僅次於棧頂的元素二者的次序。下面是其執行示例，並展示了通過給它加打印斷點來觀察其遞歸調用的規模：
$ echo 0 | dc -e '? d 2- lFx r 1- lFx +sG d1
這個過程可以進一步採用記憶化而寫為：
# i := 1
# F(x):
#   x > 1 ? M(x) : x
# M(x):
#   if x 
基於數組的保存命令:和訪問命令;，它可實現為：
1si d 2- lFx r 1- lFx +sG ;a qsN dli!
在遞歸定義中，正在計算並要記憶其結果的是，即將它的結果存儲在這裡的an中，其下標為n，而當前已存儲過的最大下標是n-1，將要存儲的下標是已經存儲的最大下標的增加1。這裡的i是數組a的已存儲過的最大下標，它的初始值是1，即假定a0和a1已經存儲了數值。這裡的F(x)在被遇到0和1這兩個參數之時，直接在代碼中返回數值，而不會用它們作為下標去訪問數組。
這個過程可以更一般化的寫為：
# a0 := 0
# a1 := 1
# i := 1
# F(x):
#   if x 
這裡的偽代碼中的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 qsN dli!
在遞歸定義的遞歸調用中，隨著的索引n的遞減，而遞歸下降至被計算出來並保存的第一項，然後沿著調用鏈逐級返回。在遞歸的每個步驟中都要計算並保存，它所需要的和這兩項之中：
*如果首先進行遞歸調用並從其返回，則也需要計算並保存，為此需要先後訪問已保存的和；
*如果首先進行遞歸調用並從其返回，則需要訪問已保存的。
下面是其執行示例，展示了通過給它加打印斷點來觀察其遞歸調用的具體步驟，其中連續高亮標示出某個過程從開始被調用直到它結束返回：
$ echo 8 | dc -e '? 0 0:a 1 1:a 1si d 2- lFx r 1- lFx +sG ;a qsN Levelnzn Fnp dli!
遞歸過程的返回值之間形成了遞推關係，每個的值被用到一次或兩次，首次是從遞歸調用直接返回，再次是對其已保存的值進行訪問，每個步驟要麼訪問兩項並保存兩項，要麼訪問一項並保存一項。故而它可以進一步寫為：
# i := 1
# F(x):
#   x > 1 ? M(x) : x
# M(x):
#   if x 
它可實現為：
1si d 2- lFx r 1- lFx +sG 2%;a qsN dli!
在遞歸定義的上述兩種求值次序中，首先進行遞歸調用的這種形式，其記憶化實現從一開始就持續進行遞歸調用直到首次遞歸返回，然後就逐級遞歸返回而不再進行遞歸調用，故而它可以進一步寫為：
# 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
下面的例子採用迭代法打印出斐波那契數列的不含第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
下面是其執行示例： 
$ echo 6 | dc -e '? si 1sa lardsa +p li1-si li0
可以將它改為計算單個的斐波那契數：
$ echo 0 | dc -e '? si 1sa lardsa + li1-si li0

**男人抑或男孩測試**
下面的例子通過男人抑或男孩測試展示dc的數組能支持遞歸運算的程度，這個測試用Python可以寫為：
import sys
sys.setrecursionlimit(1033) # 這裡設置的遞歸限制在實測時剛好夠用於k=10
def A(k, x1, x2, x3, x4, x5):
def B():
nonlocal k
k -= 1
return A(k, B, x1, x2, x3, x4)
return x4() + x5() if k 
通過手工設立並操縱簡易調用棧、非局部引用和頭等函數，這個測試可以用dc實現為：
# M0 := S := T := Z0 := K := 0
# P(): 
#   S := T + 1
#   n := z()
#   T := T + n + 1
#   MS+n := n 
#   M := U(n-1)
#   K := K + 1
#   ZK := S
# U(x, n):
#   n >= 0 ? V(n) : n
# V(x, n):
#   MS+n := x
#   U(n-1)
# Q():
#   T := T - MT - 1
#   S := T - MT
#   K := K - 1
0 0:M 0sS 0sT 0 0:Z 0sK
lT1+sS z d1+lT+sT ddlS+:M 1-lUxsM lK1+sK lSlK:ZsP 
d0!>VsU d_3RlS+:M 1-lUxsV 
lTd;M-1-sT lTd;M-sS lK1-sKsQ
# A(k, x1, x2, x3, x4, x5):
#   P(); F(); Q()
# F():
#   MS 
對於k = 10，這個測試中函數A()執行了722次，其在調用棧中的棧幀數量的最大值即遞歸深度是323層；為函數B()的創建了416個閉包，其堆疊數量的最大值是258層，併為恆等函數創建了5個閉包。將上述代碼保存入manorboy.dc文件中，下面是針對k為0到10它的執行示例： 
$ for i in $(seq 0 10); do echo $i | dc manorboy.dc; done
1
0
-2
0
1
0
1
-1
-10
-30
-67

**參見**
bc
*逆波蘭表示法
*面向堆棧編程
*隱式編程
*堆棧機器

**引用**

**外部鏈接**
軟件包 dcdate=20170312103911 在Debian GNU/Linux倉庫中
*bc的 原生Windows移植包括了dc- 嵌入到網頁中的dcdate=20171214073950 - The unix dc(1) command, this is the esoteric language that "made it big". date=20200102213208