雖然標題這麼下,但 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 的內容進行比較。
既然這資料,具自己的語義與函式群,也具有封裝性,若它能加上行為,那就更有趣了。我們下回要談到的,就是物件導向了!