dc (程序)

原作者 | Robert Morris (於AT&T貝爾實驗室) Lorinda Cherry |
---|---|
開發者 | 各種開源和商業開發者 |
程式語言 | B, C |
作業系統 | Unix, 類Unix, Plan 9 |
平台 | 跨平台 |
類型 | 命令 |
dc(desk calculator:桌面計算機)是採用逆波蘭表示法的跨平台計算機,它支援任意精度算術[1]。它是Robert Morris在貝爾實驗室工作期間書寫的[2],作為最老的Unix實用工具,先於C語言的發明。像那個年代的其他實用工具一樣,它有著一組強力的特徵和簡潔的語法[3][4]。傳統上,採用中綴表示法的bc計算機程式是在dc之上實現的。
歷史
[編輯]dc是倖存的最老的Unix語言[2]。在貝爾實驗室收到第一台PDP-11的時候,用B語言寫成的dc是在這個新機器上執行的第一個語言,甚至在組譯器之前[5]。
基本運算
[編輯]在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 <<EOF > cal.txt
4 5 *
p
EOF
$ dc cal.txt
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
。
[[equal]p]sM d5=M
這裡使用了d
命令保留原棧頂元素。其他條件有>
、!>
、<
、!<
、!=
,如果棧頂元素分別大於、不大於(小於等於)、小於、不小於(大於等於)、不等於僅次於棧頂的元素,則執行指定的宏。注意不同於Forth、PostScript和Factor,在不等式比較中的運算元的次序同在算術中的次序相反,5 3 -
等價於中綴表示法的5
-
3
,然而5 3< R
在3
<
5
時執行暫存器R
的內容。下面是有條件執行宏的範例:
$ echo 5 | dc -e '? [[equal]p]sM d5=M'
equal
$ echo 4 | dc -e '? [[equal]p]sM d5=M'
$
這裡的命令在棧頂元素等於5
之時有相應的輸出,在它不等於5
之時沒有輸出。
如果要實現一般程式語言中表達分支控制流程的條件運算子?:
或條件語句,則需要2個宏構造,例如:
$ echo 5 | dc -e '? [[equal]p q]sM [d5=M [not equal]p]x'
equal
$ echo 4 | 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
。下面是其執行範例:
$ echo 0 | dc -e '? [d-1+]sAd0!<A [1- lFx]sG [d d1<G]dsFx [* z1<H]dsHx p'
1
$ echo 9 | 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
既是迭代的遞增計數器,也是迭代二元運算的運算元,其遞增形成整數集上偏序關係的區間,同步地在這個數列上進行輸出中間值的初始值為的應用乘法運算的左側歸約,逐項輸出中間結果形成數列,這裡的x
所起到的作用也被稱為累加器(accumulator),其含義為「累計」或「累積」而不必然採用加法或乘法。它可實現為:
sn 1si 1 [lid1+si *p liln!<F]dsFx
下面是其執行範例:
$ echo 6 | dc -e '? sn 1si 1 [lid1+si *p liln!<F]dsFx'
1
2
6
24
120
720
可以將它改為計算單個的階乘:
$ echo 0 | dc -e '? sn 1si 1 [lid1+si * liln!<F]dsFx p'
1
$ echo 9 | 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
命令反轉(交換)棧頂元素和僅次於棧頂的元素二者的次序。下面是其執行範例,並展示了通過給它加列印斷點來觀察其遞迴呼叫的規模:
$ echo 0 | dc -e '? [d 2- lFx r 1- lFx +]sG [d1<G]dsFx p'
0
$ echo 20 | dc -e '? [d 2- lFx r 1- lFx +]sG [d1<G]dsFx p'
6765
$ echo 20 | dc -e '? [p d 2- lFx r 1- lFx +]sG [d1<G]dsFx' | wc -l
10945
$ echo 21 | 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
的遞減,而遞迴下降至被計算出來並儲存的第一項,然後沿著呼叫鏈逐級返回。在遞迴的每個步驟中都要計算並儲存,它所需要的和這兩項之中:
- 如果首先進行遞迴呼叫並從其返回,則也需要計算並儲存,為此需要先後訪問已儲存的和;
- 如果首先進行遞迴呼叫並從其返回,則需要訪問已儲存的。
下面是其執行範例,展示了通過給它加列印斷點來觀察其遞迴呼叫的具體步驟,其中連續突顯標示出在呼叫棧中一個過程從開始被呼叫直到它結束返回:
$ echo 8 | 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 F8
Level2 F6
Level3 F4
Level4 F2
Level5 F0
Level5 F1
Level4 F3
Level5 F1
Level5 F2
Level3 F5
Level4 F3
Level4 F4
Level2 F7
Level3 F5
Level3 F6
$ echo 8 | 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 F8
Level2 F7
Level3 F6
Level4 F5
Level5 F4
Level6 F3
Level7 F2
Level8 F1
Level8 F0
Level7 F1
Level6 F2
Level5 F3
Level4 F4
Level3 F5
Level2 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
下面是其執行範例:
$ echo 6 | dc -e '? si 1sa [lardsa +p li1-si li0<F]sF 0 li0<F'
1
1
2
3
5
8
可以將它改為計算單個的斐波那契數:
$ echo 0 | dc -e '? si 1sa [lardsa + li1-si li0<F]sF 0 li0<F p'
0
$ echo 9 | dc -e '? si 1sa [lardsa + li1-si li0<F]sF 0 li0<F p'
34
參見
[編輯]參照
[編輯]- ^ Linux使用者命令(User Commands)手冊頁 : an arbitrary precision calculator –
- ^ 2.0 2.1 Brian Kernighan and Ken Thompson. A nerdy delight for any Vintage Computer Fest 2019 attendee: Kernighan interviewing Thompson about Unix. YouTube. 事件發生在 29m45s. [September 3, 2019]. (原始內容存檔於2022-02-01).
- ^ The sources for the manual page for 7th Edition Unix dc. [2020-09-25]. (原始內容存檔於2019-09-24).
- ^ Ritchie, Dennis M. The Evolution of the Unix Timesharing System. Sep 1979 [2019-05-31]. (原始內容存檔於2010-05-06).
- ^ McIlroy, M. D. A Research Unix reader: annotated excerpts from the Programmer's Manual, 1971–1986 (PDF) (技術報告). CSTR. Bell Labs. 1987 [2019-05-31]. 139. (原始內容存檔 (PDF)於2019-11-30).