示例:扑克牌

用抽象数据类型构建扑克牌、手牌和牌堆的接口,并演示更换实现不影响上层代码

如果我们能用抽象数据类型表示纸牌游戏,那多酷?让我们创建一个比有理数更复杂的接口,能表示牌、手牌和牌堆。有了这些抽象,我们就能玩一些简单的纸牌游戏了!

创建牌

看任何一张牌,标识它为扑克牌的两个属性是点数(rank)和花色(suit)。当然,你还能观察到其他属性,比如长方形形状或塑料质感,但那些不是用来识别牌的重要特征。因此,这是我们的 make-card 构造函数,接受一个 rank 和一个 suit:

(define (make-card rank suit)
  (cons rank (first suit)))

这是它的选择函数:

(define (rank card)
  (car card))

(define (suit card)
  (cdr card))

于是我们可以这样创建一张牌并提取属性:

-> (define c (make-card 13 'heart))
card
-> (rank c)
13
-> (suit c)
h

我们刚创建了红桃 K。

创建手牌

就像现实中一手牌是一组牌,在我们的抽象中,手牌将是牌的列表。我们定义了构造函数和选择函数如下:

(define make-hand list) ;; 构造函数创建牌的列表

(define first-card car) ;; 返回手牌中的第一张牌

(define rest-hand cdr) ;; 返回手牌中其余的牌

(define empty-hand? null?) ;; 检查手中是否没有牌

注意我们把 make-hand 定义成指向过程 list 的变量。这是因为我们不想指定 make-hand 该接受几个实参——我们可以创建任意长度的手牌。我们只想让 make-hand 接受任意数量的牌并存入列表。下面是调用我们 ADT 的一些示例:

-> (define my-hand (make-hand (make-card 1 'heart)
                               (make-card 5 'diamond)
                               (make-card 10 'diamond)
                               (make-card 13 'club)))
my-hand
-> (first-card my-hand)
(1 . h)
-> (rest-hand my-hand)
((5 . d) (10 . d) (13 . c))

使用我们的实现

表示牌就这些!你有牌,也有一组牌。其他一切都能用这两个对象定义。例如,牌堆就是每种点数、花色组合各一张牌的手牌(再加两张王,不过暂时略过)。

现在用我们的实现写些过程。大多数纸牌游戏中,牌的点数代表它的数值。我们来写一个求手牌总点数(total value)的过程。total 接受一手牌,返回所有牌的点数之和。

(define (total hand)
    (if (empty-hand? hand)
        0
        (+ (rank (first-card hand)) (total (rest-hand hand)))))

例子:

-> (total my-hand)
29

更换实现

如果我们改变表示牌的方式会怎样?我们的 total 代码还能工作吗?

答案是肯定的,total 仍能工作,因为有一层抽象把它与牌或手牌的 implementation 隔开。只要我们保持构造函数和选择函数的名字不变,所有基于它构建的过程都会继续工作。假设我们把表示牌的方式改成这样:

(define (make-card rank suit)
  (cond ((equal? suit ’heart) rank)
        ((equal? suit ’spade) (+ rank 13))
        ((equal? suit ’diamond) (+ rank 26))
        ((equal? suit ’club) (+ rank 39))
        (else (error "say what?")) ))

(define (rank card)
  (remainder card 13))

(define (suit card)
  (nth (quotient card 13) ’(heart spade diamond club)))

我们的 total 过程在这个实现下同样能工作。在 Racket 解释器里试试!

用这种编程风格,我们能创建更大的程序。