Common Lisp學習筆記(十)

10 Assignment

lisp全局變量的命名一般在首尾加上**,eg,*total-glasses*數據結構

 

10.3 stereotypical updating methods

更新一個變量的值常常會利用該變量原來的值作一些計算獲得新的值再用來更新,這裏有2種狀況,增長或者減小一個計數器,在list的頭部增長或者刪除一個元素app

INCF and DECF macros
> (setf a 2)
2
> (incf a 3)
5
> (decf a)
4
PUSH and POP macros

(setf x (cons 'foo x))能夠寫成(push 'foo x),相似的在頭部刪除一個元素能夠用(pop x),不只刪除第一個元素還會將其返回less

爲了保證代碼的可讀性和優雅性,儘可能不要修改局部變量的值,能夠let新的變量去代替修改dom

 

10.4 WHEN and UNLESS

(when test
    body)

(unless test
    body)

when首先判斷test是否爲真,若是爲nil則直接返回nil,不然進入body執行全部語句最後返回最後一個語句的值。unless的用法相似,可是test爲nil才進入body函數

好比如今有一個函數有2個參數x,y,要保證x爲odd且y爲even,若是不是則將它們加或減1輸出信息,而後將它們相乘spa

(defun picky-multiply (x y)
  (unless (oddp x)
    (incf x)
    (format t "~&changing x to ~S to make it odd." x))
  (when (oddp y)
    (decf y)
    (format t ...))
  (* x y))

 

10.5 generalized vars

generalized variable是一個指針能夠存儲的任意空間。日常所說的變量都有一個指針指向它的值,好比說n的值爲3,意思是名爲n的變量有一個指針指向數字3.修改變量的值只是將指針換爲指向另外一個對象的指針指針

思考例子:code

(setf x '(jack benny was 39 for many years))
(setf (sixth x) 'several)

> x
(jack benny was 39 for several years)

> (decf (fourth x) 2)
37

> x 
(jack benny was 37 for ...)

例子中的修改變量的值只是把一個cons的car指針改成指向別的對象的指針orm

 

10.6 tic-tac-toe

九個格子當前的狀態須要數據結構來存儲,這裏用list實現。對於每個格子,0表示該格子爲空,1表示上面是O,10表示上面是X. make-board()建立一個空的九宮格對象

(defun make-board ()
  (list 'board 0 0 0 0 0 0 0 0 0))

list的第一個元素是'board,(nth 2 B)能夠訪問第二個格子

print-board函數以九宮格的形式輸出當前的棋盤狀態,須要輔助函數將數據結構中的0,1,10轉化爲' ','O', 'X'

(defun convert-to-letter (v)
  (cond ((equal v 1) "O")
        ((equal v 10) "X")
        (t " ")))

(defun print-row (x y z)
  (format t "~&  ~A | ~A | ~A"
    (convert-to-letter x)
    (convert-to-letter y)
    (convert-to-letter z)))

(defun print-board (board)
  (format t "~%")
  (print-row (nth 1 board) (nth 2 board) (nth 3 board))
  (format t "~& ------------")
  (print-row (nth 4 board) (nth 5 board) (nth 6 board))
  (format t "~& ------------")
  (print-row (nth 7 board) (nth 8 board) (nth 9 board))
  (format t "~%~%"))

make-move()表示在某個格子下一步棋,參數player爲1或者10表示下子'O'或者'X'

(defun  make-move (player pos board)
  (setf (nth pos board) player)
  board)

顯然這裏並無判斷pos位置是否已經有下過

假設咱們是人機對戰,10表明計算機,1表明玩家,定義全局變量

(setf *computer* 10)
(setf *opponent* 1)

 

前面幾個函數已經能夠實現由機器或者玩家在某個位置下一步棋,下面要判斷遊戲何時結束。咱們先將3個子連成直線的位置狀況列舉在全局變量*triplets*中

(setf *triplets*
  '((1 2 3) (4 5 6) (7 8 9)
    (1 4 7) (2 5 8) (3 6 9)
    (1 5 9) (3 5 7)))

連成直線的只有8種狀況,咱們須要一一對三個位置的數進行求和來判斷是否有連成直線,sum-triplet給定一個triplet計算三個位置的和,compute-sums返回8種狀況求和的一個list

(defun sum-triplet (board triplet)
  (+ (nth (first triplet) board)
     (nth (second triplet) board)
     (nth (third triplet) board)))

(defun compute-sums (board)
  (mapcar #'(lambda (triplet) (sum-triplet board triplet)) *triplets*))

由於雙方由1和10表示,因此只有直線三個位置和爲3或者30才能贏,winner-p判斷是否有一方獲勝

(defun winner-p (board)
  (let ((sums (compute-sums board)))
    (or (member (* 3 *computer*) sums)
        (member (* 3 *opponent*) sums))))

程序的主函數

(defun play-one-game ()
  (if (y-or-n-p "would you like to go first?")
      (opponent-move (make-board))
      (computer-move (make-board))))

程序詢問用戶是否要先手,而後根據選擇來決定是opponent-move仍是computer-move

 

剩下的問題就是用戶和電腦的一步該怎麼走了,用戶的話天然就是詢問用戶的輸入,判斷輸入合法性,等用戶走完一步再判斷是否分出勝負仍是和局。

(defun opponent-move (board)
  (let* ((pos (read-a-legal-move board))
         (new-board (make-move *opponent* pos board)))
    (print-board new-board)
    (cond ((winner-p new-board) (format t "~&you win!"))
          ((board-full-p new-board) (format t "~&tie game."))
          (t (computer-move new-board)))))

read-a-legal-move判斷用戶輸入是否在1到9,還有該格子是否爲空,函數遞歸調用直到用戶接受到合法輸入

(defun read-a-legal-move (board)
  (format t "~&your move: ")
  (let ((pos (read)))
    (cond ((not (and (integerp pos) (<= 1 pos 9)))
           (format t "~&invalid input.") (read-a-legal-move board))
          ((not (zerop (nth pos board))) 
           (format t "~&that space is already occupied.") (read-a-legal-move board))
          (t pos))))

判斷是否還有空位只要查看是否有0的空格便可

(defun board-full-p (board)
  (not (member 0 board)))

computer-move的狀況就稍微複雜,choose-best-move返回兩個元素的list,第一個是選擇的位置,第二個是作這樣的選擇所用的策略,即一個用來提示的string

(defun computer-move (board)
  (let* ((best-move (choose-best-move board))
         (pos (first best-move))
         (strategy (second best-move))
         (new-board (make-move *computer* pos board)))
    (format t "~&my move: ~S." pos)
    (format t "~&my strategy: ~S." strategy)
    (print-board new-board)
    (cond ((winner-p new-board) (format t "~&I win!"))
          ((board-full-p new-board) (format t "~&tie game"))
          (t (opponent-move new-board)))))

choose-best-move函數要判斷到底在哪個位置來走下一步,而不是要求用戶輸入。一個方法是隨機選擇一個空位,這種方法可行,可是會智能程度比較低,有可能用戶已經有兩個相連了電腦還不會去阻止他而是隨便放一個位置。如今使用另外一種策略,若是電腦已經有兩個相連,就會嘗試是否能獲勝。不然,會查看用戶是否已經有兩個相連,若是有會嘗試阻止用戶獲勝

win-or-block判斷triplets的八種狀況中有沒有和等於target-sum的狀況,若是有則let中的triplet不爲nil,最後就會進入(find-empty-position board triplet)中來尋找空位填在那個triplet中,注意這裏不論是電腦查找本身獲勝仍是阻止用戶獲勝都適用. find-empty-position則很簡單,返回triplet中空的那個pos

(defun win-or-block (board target-sum)
  (let ((triplet (find-if 
                  #'(lambda (trip) (equal (sum-triplet board trip) target-sum))
                  *triplets*)))
    (when triplet (find-empty-position board triplet))))

(defun find-empty-position (board squares)
  (find-if #'(lambda (pos) (zerop (nth pos board))) squares))

下面兩個函數分別調用win-or-block嘗試查找須要填在triplet惟一那個空位的狀況,若是有就將pos與策略組成list返回,若是沒有則pos就是nil,and語句中第一個就會返回nil

(defun make-three-in-a-row (board)
  (let ((pos (win-or-block board (* 2 *computer*))))
    (and pos (list pos "make three in a row"))))

(defun block-opponent-win (board)
  (let ((pos (win-or-block board (* 2 *opponent*))))
    (and pos (list pos "block opponent"))))

最後的choose-best-move,若是前兩種狀況都返回nil則使用隨機選擇一個空位的策略

(defun choose-best-move (board)
  (or (make-three-in-a-row board)
      (block-opponent-win board)
      (random-move-strategy board)))

(defun random-move-strategy (board)
  (list (pick-random-empty-position board) "random move"))

(defun pick-random-empty-position (board)
  (let ((pos (+ 1 (random 9))))
    (if (zerop (nth pos board)) 
        pos
        (pick-random-empty-position board))))

總結一下,lisp中除了修改全局變量的值之外,通常不要修改局部變量的值,而是用let綁定一個新變量,這樣的assignment-free的程序纔是優雅的

ex 10.6

> (setf x nil)
nil
> (push x x)
(nil)
> (push x x)
((nil) nil)
> (push x x)
(((nil) nil) (nil) nil)

 

10.7 list srugery

咱們能夠直接修改cons的cdr指針來改變list的結構,例如能夠將一個cons的cdr指向本身,產生一個環的結構:

> (setf circ (list 'foo))
(foo)

> (setf (cdr circ) circ)
(foo foo foo ...)

 

10.8 destructive operations on lists

destructive list operation是改變cons cell內容的操做,雖然可能帶來一些危險如一些共享結構的修改可能形成沒法預測的後果,這種操做有時候也是很是有用的,這些函數通常以F開頭

nconc

append的做用相似,區別是append其實是建立一個新的list,而nconc是直接修改第一個list的最後一個cons的cdr來指向第二個list

特殊狀況:若是(nconc x y)中x是nil,則直接返回y,而不去改變x的值

nsubst

subst的destructive版本

> (setf tree '(i say (e i (e i) o)))
(i say (e i (e i) o))

> (nsubst 'a 'e) tree)
(i say (a i (a i) o))

> tree
(i say (a i (a i) o))

> (nsubst 'cheery '(a i) tree :test #'equal)
(i say (a i cherry o))

最後兩行的替換要使用#'equal對值進行比較而不是默認的eql比較地址

其它一些destructive func
  • nreverse
  • nunion
  • nintersection
  • nset-difference
  • delete: remove

ex 10.9

(defun shop (x)
  (if (consp x) (setf (cdr x) nil))
  x)

ex 10.10

(defun ntack (x y)
  (nconc x (list y)))

 

10.10 setq and set

setq是比較古老的用法,做用和setf相同

set將一個值存儲到一個symbol的value cell中,至關與修改這個全局變量的值

symbol-value返回一個symbol的value cell中的值

(setf duck 'donald)

(defun test1 (duck)
  (list duck (symbol-value 'duck)))

(test1 'huey) -> (huey donald)

(defun test2 (duck)
  (set 'duck 'daffy) ; 修改全局變量的值
  (list duck (symbol-value 'duck)))

(test2  'huey) -> (huey daffy)
相關文章
相關標籤/搜索