雖然標題這麼下,但 Racket 的確有物件導向!今明兩天,我們要介紹 Racket 的抽象機制,包含它的資料抽象機制:Struct 與物件導向。
想像一下,我們為什麼需要資料抽象?我們已經有 HashTable 了,或簡單點,也有 List、Vector 了,為什麼需要更高層次地對資料進行抽象處理呢?
我們來看範例程式:
(struct student (id name gender))
(define racket (student 1 "Racket" 'M))
(define julia (student 2 "Julia" 'F))
(define ada (list 3 "Ada" 'F))
(define haskell (hash id 3 name "Haskell Curry" gender 'M))
(student? racket) ;; #t
(student? ada) ;; #f
(student? haskell) ;; #f
(list? ada) ;; #t
(list? julia) ;; #f
(hash? haskell) ;; #t
(hash? racket) ;; #f
(student-name racket) ;; "Racket"
(student-gender julia) ;; 'F
我們在此定義了一個簡單的 struct
— student,並且宣告了 racket、julia 為 student struct
,並且宣告兩個不太合群的 ada
與 haskell
(寫這兩個語言的人別生氣唷),一個為 list
,一個為 hash
。
我們定義完了之後,在下方看到使用 student?
直接進行比較,有別於 list?
或 hash?
,後者是使用通用型資料結構進行資料的組織,而前者卻是具有 語義 的資料型態。這就是我們所說,資料抽象化之後的作用。
因此,當你把你的資料轉成 struct
,它可以擁有自己的語義與存取方式,如第三段程式碼一般。
Racket 可是很具有現代感的語言呢!struct 可以有繼承關係,例如以下範例:
(struct position (x y))
(struct 3d-position position (z))
(define point (3d-position 1 2 3))
(3d-position-x point) ;; 錯誤!
(position-x point) ;; 1
(3d-position-z point) ;; 3
第一段程式可以看到,我們先定義了 position
,再定義了另一個 3d-position
,名稱的後頭接著要繼承的 position
,3d-position
就能繼承 position
的內容。
但是,能否透過 3d-position
來存取 position
的內容呢?在 Racket 的原則裡,是不行的!要存取 position
的內容(x
與 y
)還是要透過 position
才可以!
那麼,資料之間怎麼比較呢?在 Racket 裡頭,有 eaual?
、eq?
、eqv?
等不同的比較方式,我們現在討論最常見的 equal?
,可以參考以下範例:
(struct position-t (x y) #:transparent)
(struct position (x y))
(define pos-t1 (position-t 1 2))
(define pos-t2 (position-t 1 2))
(define pos-1 (position 1 2))
(define pos-2 (position 1 2))
(equal? pos-1 pos-t1) ;; #f
(equal? pos-1 pos-2) ;; #f
(equal? pos-t1 pos-t2) ;; #t
我們在這裡定義了兩個很類似的 struct
,一個有宣告 #:transparent
,一個沒有。然而各位可以看到,第一個 equal?
理所當然地拿到了個 #f
,但第二個為什麼是 #f
呢?而第二個 #f
但在第三個 equal?
比較時,卻又是 #t
?
在這裡閉上眼思考思考,程式語言給予的這個資料抽象機制,它不只讓你可以定義具有語義與結構的資料型態,更具有保護資料內部狀態的封裝性。因此,每個 struct
都是預設為 不透明的 狀態(opaque),外部的 equal?
無法直接取得它的內容以進行比較,操作這個資料的人,只能透過這個資料提供的方式來存取它。反之,就是 透明的 (transparent)狀態,equal?
在外部,可以對 struct
的內容進行比較。
既然這資料,具自己的語義與函式群,也具有封裝性,若它能加上行為,那就更有趣了。我們下回要談到的,就是物件導向了!