Racket 的數值系統,核心架構仍然是以 Scheme 的為基礎。在 Scheme 的規格書 [1] 裡頭,列舉了以下的數值類型,其範圍由上到下限縮:
最上方的 number 是最廣的,以集合論來說,就是下面四項的宇集。因此,你在任何地方,問 Racket 一個數字是否是 number 時,一定會得到 true,不管它是什麼樣的數字。
接下來,可以說是這門從學術界發展出來的語言的特色:可以直接進行複數操作,不必透過特殊的函式庫。
複數以降,就是實數,數學上來看,這範圍包含有理數與無理數,或正數與負數。無理數就是無法算至小數點下某一位開始循環的數字,例如圓周率、黃金分割、根號 2。有理數就是會在某一位小數開始循環,並能轉成分數的數字。因此,實數可以是無理數、複數、有理數、正數。
實數之下便是有理數,在 Racket 裡的有理數,真的就是分數,不信的話,你可以執行看看:
(/ 1 3)
最後,是整數,整數包含了正整數與負整數
我們來試著算算看圓面積,用 高解析度圓周率,取到小數點以下 25 位數(到第三行),然後用半徑為 5
來算圓周看看:
(define pi 3.1415926535897932384626433)
(* 5 5 pi)
最後運算產生結果時,理論上來說,至少應該要到小數點以下 25 位,但你會看到小數位數在某個地方就停了:
78.53981633974483
這是因為,在電腦裡面,浮點數小數位數會因為實作機制可容許的數值空間有限,使得小數位數無法想到幾位就到幾位。不管是在 Racket、Scheme、Lisp、Python、Java 都會如此。
然而,在 Racket/Scheme 裡頭,有一種特殊的數值型態,稱為 exact
,這不是說在上述的數值類別之外還有一個類別,它是一種精確度表示法,相反地,也有個 inexact
。exact
可以用以下方式來呈現:
;; 用 #e 來表示 exact:
(define pi #e3.1415926535897932384626433)
(* 5 5 pi)
改寫了這程式,重新運行後,你會發現結果變成如下:
215926535897932384626433
78 ------------------------
400000000000000000000000
Racket 會以有理數來呈現計算結果,若直接印出 pi
的話,會是這樣的值:
> pi
1415926535897932384626433
3 --------------------------
10000000000000000000000000
換句話說,它會把你給它不論多少位數的小數,完整地用有理數保存下來,好使你在計算時,可以精準到你要的程度。那另一個角度來說,inexact
就不是如此,它是 Racket 裡頭浮點數的預設操作與表示方式(雖然也可以用 #i
的方式來指定)。inexact
的操作就像原本的圓面積計算一樣,不會精準到你給的小數位數。
這張圖它在表示只有機器人能答對這個問題:0.1 + 0.2
,你可以在 Racket 裡試試:
(+ 0.1 0.2)
為何若用 inexact
無法進行精準的小數點處理呢?甚至 0.1 + 0.2
這個值沒辦法得到一個正確的結果呢?這是計算機概論書本一定會談到的電腦浮點運算所使用的 IEEE 754 系統 [2],簡單來說,因為電腦是用二進位資料進行數值資料儲存,在從十進位轉換到二進位時,會產生無法完整轉換的問題。因此在進行 0.1 + 0.2
時,會發生從二進位轉十進位的精確度誤差。
這時候,你可以想見,或許用上方的 exact
型態來處理就可以了:
(+ #e0.1 #e0.2)
是的,雖然這可以算出正確值,還是必須說明:這是用計算效能得來的結果。inexact
的計算只需要透過底層原有的計算機制來進行,而exact
的計算卻需要對數值進行一層包裝,好使得數值不會受到小數位數與整數位數的影響。這當然就需要相當的效能作為代價。 然而,我們在這不完美的世界解決各項問題,本來就需要針對不同的處境權衡解決之道。
除了以上談到的常用數值型態,Racket 像 Python、Ruby 之類的語言,支援了無限長度的整數位數,例如你可以把 pi
的小數點拿掉:
(define pi 31415926535897932384626433)
(* 5 5 pi)
這樣仍然可以得到一個正確的結果,這是因為一旦整數位數超過了平台提供的長度,它會自動轉為 bignum
型態,這點倒是不用像 exact
與 inexact
那樣用外在的字面方式來區別。
回到前頭,Racket 也支援複數運算,複數的表示用 i
,如下:
(* 1+1i 2+2i)