Lisp-Stat 翻譯 —— 第六章 面向對象編程

第六章 面向對象編程

經過提供一個object數據類型和一些用於處理對象的函數,Lisp-Stat支持面向對象編程。在第二章裏,咱們看到Lisp-Stat繪圖和模型是用對象實現的,咱們使用一些消息來檢測和修改這些對象。本章將更加深刻地描述Lisp-Stat對象系統。該系統專門設計用來處理交互統計工做,與Lisp社區裏使用的其它對象系統有些不一樣,其它的對象系統將在第6.7節裏簡要討論。 編程

6.1 一些動機

假設咱們想要創建一個智能函數用來描述或者繪製一個數據集。最初,咱們能夠考慮三個數據類型:單樣本、多樣本和成對樣本。那麼describe-data函數能夠編寫成這樣: 數組

(defun describe-data (data)
    (cond
      ((single-sample-p data) ...)
      ((multiple-sample-p data) ...)
      ((paired-sample-p data) ...)
      (t (error "don't know how to describe this data set"))))
你也能夠用類似方式定義一些其它函數,例如,一個plot-data函數。

    假設後來咱們決定想要處理數據集的第四個數據類型,一個簡單的時間序列。爲了容納這個新的數據類型,咱們不得不編輯describe-data函數和plot-data函數。這就有幾個缺陷了,首先,咱們須要會的並可以理解這些函數的源代碼,這在當前這個小例子裏不是個問題,可是在處理更復雜的設置的時候會製造很大的困難。其次,僅僅爲了達到增長處理新數據類型的能力而編輯源代碼,咱們要承擔處理現有數據類型是代碼中斷的風險。 數據結構

    一個備選策略是去安排數據集自己去了解如何描述本身,換句話說,可以得到合適的代碼段已打印對於自身的描述。那麼describe-data函數只不過讓數據集去找出合適的代碼段並執行之。增長數據集的一個新的類型不須要對describe-data函數進行任何修改,也不須要對處理現有數據類型的代碼作任何修改;咱們僅須要針對新的數據類型編寫和安裝合適的代碼。這是面向對象編程方法背後的一個主要的思想。定位和執行合適的代碼段的請求過程叫作消息傳遞。代碼段本書就是消息的方法調用。 閉包

    面向對象編程的另外一個關鍵思想是繼承。一個對象的新類型與現存對象在細節上的區別可能很小。例如,一個時間序列可能須要它本身的描述方法,可是可能可使用相同的繪圖方法做爲配對樣本。爲了發揮這個類似性的最大優點,咱們能夠在一個階層裏安排一個對象,同時每一個對象都從這個對象繼承,它是它們的祖先。那麼若是一個對象沒有它本身的方法,處理消息的系統就會被引導去使用其祖先的方法。那麼,當咱們開發一個新對象,若是沒有可用的繼承方法或者繼承的方法不適合的時候,咱們僅須要提供一個新的方法就能夠了。這種重用現有代碼的能力在減小設計新對象所須要的時間上將減小一個數量級。經過確保被不少對象使用的方法之間自動地傳遞消息的改進,一樣能夠方便代碼維護。 app

6.2 對象和消息

6.2.1 基礎結構和術語

Lisp-Stat對象是爲面向對象編程專門設計的獨立的數據類型。經過使用objectp謂詞,你能夠確認一個數據項是否是對象。 框架

    一個對象是一個數據結構,它在所謂的slots的指定位置包含對象的信息。在這方面,它與C或者S結構體是類似的。另外在處理數據方面,對象能夠應答消息,讓他們作必定的動做。在形如如下格式的表達式裏,消息使用函數send發送給對象:(send <object> <selector> <arg 1> ... <arg n>)。
<selector>是一個符號,一般是一個關鍵字符號,用來識別一個消息。舉個例子,假設變量p指的是第二章裏介紹的那個histogram函數返回的一個直方圖對象,那麼表達式(send p :add-points (normal-rand 20))向直方圖發送一個消息,讓它向本身加入一個有20個標準隨機分佈變量的樣本,消息對應的message selector是:add-points;消息的參數是由表達式(normal-rand 20)產生的正態變量的樣本。消息由選擇器和參數組成。用來響應這個消息代碼就是針對消息的方法。經過向對象和消息選擇器分發(dispatching),send函數會找出那個合適的方法。 函數

    對象在一個繼承的層次裏被組織起來。在這個層次裏的頂層是根對象,就是全局變量*object*的值。這個對象包含一些方法,用來對全部對象使用一些標準消息。例如,消息:own-slots返回一個對象裏的用來命名槽的符號列表。根對象裏的槽是: 工具

> (send *object* :own-slots)
(DOCUMENTATION PROTO-NAME INSTANCE-SLOTS)
消息:slot-value帶一個參數——一個命名槽的符號,返回儲存在槽裏的當前值:

> (send *object* :slot-value 'proto-name)
*OBJECT*
> (send *object* :slot-value 'instance-slots)
NIL
documentation槽用來保存一些信息,該信息會由:help消息使用。

    根對象是一個原型對象,設計根對象是用來充當一個構造其它對象的模板,也叫作原型的實例。槽proto-name包含一個命名這個原型的符號,該槽的值在產生一個對象的打印體表示時被用到:
post

> *object*
#<Object: 145d040, prototype = *OBJECT*>
原型對象與其它全部對象很像,除了它包含一些構造實例須要的附加信息。尤爲是,想要像一個原型對象同樣被使用,該對象應該包含一個叫作instance-slots的槽。該槽的值是一個符號列表,表示將被安裝到一個原型的實例裏的全部槽,這個原型也應該包含響應列表裏每個符號的槽。在包含在這些原型的槽裏的值這點上,原型的實例與原型是不一樣的,原型的實例之間也是不一樣的。

    當對象發送一個消息時,它首先檢查它本身是否有一個處理該消息的方法。若是它本身有,它就用這個方法。不然,它將遍歷它的祖先直到找到該方法。若是沒有找到這樣的一個方法,將發送一個錯誤。在肯定一個槽的值的時候,也是用了相同的處理過程:首先,對象檢測本身的槽,而後是其祖先的槽。這裏對象的租箱被檢查的順序叫作對象的優先級列表。這個優先級列表一般包含對象自己做爲第一個元素,包含根對象做爲最後一個元素。在本節的這個簡單的例子裏,能夠認爲每一個對象僅有一個直接祖先,或者叫父類。本例中,優先級列表是由該對象、該對象的父對象、父對象的父對象、等等,直到根對象位置。更加複雜的例子將在後標的第6.6節中討論。
測試

6.2.2 構造新的對象

新的對象一般經過向一個原型第一項裏發送:new消息來構造。這個消息須要的參數的數量由原型來決定;對於根節點,它不須要參數。舉個例子,咱們可使用如下表達式來構造一個表示一個數據集的對象:

> (setf x (send *object* :new))
#<Object: 147e0bc, prototype = *OBJECT*>
    初始狀況下,一個從*object*原型裏建立的對象是沒有它本身的槽的:
> (send x :own-slots)
NIL
你可使用:add-slot消息爲一個對象添加槽,該消息須要一個參數,一個命名槽的符號。你能夠在一個帶額外的可選參數的槽裏放置你指定的值,若是沒有指定值,該槽的默認值爲nil。讓咱們添加一個包含一個正態隨機數字的槽data,在添加一個容納對咱們的數據集的描述性標題的槽title:
> (send x :add-slot 'data (normal-rand 50))
(-0.961671261245195 -0.03073969289405536 1.5577613346793322 0.5292481826230735 -0.3462433415969322 0.8429198212376022 0.8955459122132918 -0.6722222042240655 -0.2258089379905481 -0.7424918965036337 -0.7065091306089195 1.4569458955136176 1.8034388173374396 -1.0011915353362055 -1.1439767529393954 0.23075866234223663 -0.04478652198931416 -0.6938743027004384 0.27549371131380973 -0.12461730393232302 0.9154031052901357 -0.7901457891598227 -1.0528183854817184 -0.1611916129413438 -0.5570674445463484 2.026709492605648 -1.8321937303176188 -0.057280152706097424 0.14707120651936426 1.6974462077365668 1.114021890943337 0.4157649206799311 1.0151465340915484 -1.1710331835472994 1.151784316238704 -1.350950765281483 -1.0910310770474871 2.659543772302781 1.5208132663438354 0.026963615535911336 0.6286342472708142 -1.6753192017970124 -0.7329778544654284 -1.66605746948027 -1.477241171430288 -0.6115453732205446 0.047807107535505836 0.2553196181944672 0.6777721882239728 0.1643399828719198)
> (send x :add-slot 'title)
NIL
> (send x :own-slots)
(TITLE DATA)
    :slot-value消息用來檢測一個對象的槽的值:
> (send x :slot-value 'data)
(-0.961671261245195 -0.03073969289405536 1.5577613346793322 0.5292481826230735 -0.3462433415969322 0.8429198212376022 0.8955459122132918 -0.6722222042240655 -0.2258089379905481 -0.7424918965036337 -0.7065091306089195 1.4569458955136176 1.8034388173374396 -1.0011915353362055 -1.1439767529393954 0.23075866234223663 -0.04478652198931416 -0.6938743027004384 0.27549371131380973 -0.12461730393232302 0.9154031052901357 -0.7901457891598227 -1.0528183854817184 -0.1611916129413438 -0.5570674445463484 2.026709492605648 -1.8321937303176188 -0.057280152706097424 0.14707120651936426 1.6974462077365668 1.114021890943337 0.4157649206799311 1.0151465340915484 -1.1710331835472994 1.151784316238704 -1.350950765281483 -1.0910310770474871 2.659543772302781 1.5208132663438354 0.026963615535911336 0.6286342472708142 -1.6753192017970124 -0.7329778544654284 -1.66605746948027 -1.477241171430288 -0.6115453732205446 0.047807107535505836 0.2553196181944672 0.6777721882239728 0.1643399828719198)
> (send x :slot-value 'title)
NIL
:slot-value消息也接收第二個參數,它將被做爲槽的新值插入。咱們可使用這個辦法將一個標題放置到咱們的數據集的title槽裏:
> (send x :slot-value 'title "a data set")
"a data set"
> (send x :slot-value 'title)
"a data set"
    經過向對象裏發送一個不帶參數的:delete-slot消息,從一個對象裏刪除一個槽是可能的,命名槽的符號將被刪除。

    除了它本身的槽,對象x也能獲取被它繼承的或者共享的根對象的槽:

> (send x :slot-value 'proto-name)
*OBJECT*
:slot-names消息返回一個對象的全部可讀取的槽的列表,包括對象的槽和它繼承來的槽:
> (send x :slot-names)
(TITLE DATA DOCUMENTATION PROTO-NAME INSTANCE-SLOTS)
:has-slot消息可用來肯定一個對象是否能夠獲取一個特定名稱的槽:
> (send x :has-slot 'title)
T
> (send x :has-slot 'x)
NIL
> (send x :has-slot 'proto-name)
T
默認狀況下,該消息的方法除了檢查對象本身的槽還檢查繼承來的槽。若是使用了值爲t的關鍵字:own,那麼僅檢測對象本身的槽:
> (send x :has-slot 'title :own t)
T
> (send x :has-slot 'proto-name :own t)
NIL

一個槽是屬於一個對象全部,仍是從其餘對象繼承而來僅在咱們想要刪除該槽或者改變槽的值的時候纔是重要的。經過發送一個合適的消息給槽的全部者,你僅能刪除該槽或者改變該槽的值。要求一個對象改變一個繼承的槽的值會引起一個錯誤。例如:

> (send x :slot-value 'proto-name 'new-name)
Error: object does not own slot - PROTO-NAME
Happened in: #<Byte-Code-Closure-SLOT-VALUE: #149d2b0>
練習 6.1

略。

6.2.3 定義新的方法

初始狀況下,一個對象能夠應答一些標準消息,就像咱們用過的同樣。針對這些消息的方法是從根對象繼承而來的。就像在第2.7節裏簡要概述過得同樣,經過使用defmeth宏能夠爲一個對象定義新的方法。defmeth表達式看起來像這樣:(defmeth <object> <selector> <parameters> <body>)。參數<object>應該是一個能夠求值成對象的表達式,其他的參數不會被求值,<parameters>是命名方法的參數的符號的列表。參數列表裏能夠包括&key、&optional和&rest類型的參數。若是方法體是由字符串開始的,那個字符串將被看做爲一個文檔字符串,而且被安裝供:help消息使用。

    在爲一個消息寫一個方法時,你一般須要可以引用到接收消息的那個對象。由於方法是能夠被繼承的,你沒法確認一個對象被寫的時候接收對象的身份。爲了解決這個問題,對象系統確保當方法體被求值時,變量self是與接收消息的對象是綁定的。舉個例子,經過使用如下定義,針對:discribe消息,咱們能夠給定數據集一個方法:

> (defmeth x :describe (&optional (stream t))
    (format stream "This is ~a~%"
            (send self :slot-value 'title))
    (format stream "The sample mean is ~g~%"
            (mean (send self :slot-value 'data)))
    (format stream "The sample standard deviation is ~g~%"
            (standard-deviation
             (send self :slot-value 'data))))
:DESCRIBE
變量self用來提取對象的數據和標題。如今,新的:describe消息能夠像其餘消息同樣來使用了:
> (send x :describe)
This is a data set
The sample mean is 2.271335432522105E-2
The sample standard deviation is 1.0647789521240567
NIL
默認地,該方法打印標準輸出。可選的流參數能夠用來指定一個替代的輸出流。

    變量self能夠被視爲對一個方法的強制的首參數。它能夠針對關鍵字參數和可選參數,被用在默認表達式中。若是在方法裏創建了一個函數閉包,綁定到正在接收消息的對象上的變量self是被包含在閉包的環境中的。

    有一些函數,它們僅能在方法體內部使用。一個這樣的函數就是slot-value,這個函數帶一個參數,一個命名當前對象裏的槽的符號,返回這個槽的值。在一個方法體裏,你也可使用slot-value函數做爲setf函數的要設置的位置。使用slot-value函數而不是:slot-value消息一般會產生較快的代碼;:slot-value消息主要的意圖是從解釋器裏檢測對象,那裏slot-value函數是不能使用的。使用這個函數,咱們能夠將咱們本身的函數定義以下:

> (defmeth x :describe (&optional (stream t))
    (format stream "This is ~a~%" (slot-value 'title))
    (format stream "The sample mean is ~g~%"
            (mean (slot-value 'data)))
    (format stream "The sample standard deviation is ~g~%"
            (standard-deviation (slot-value 'data))))
:DESCRIBE

這種定義的一個缺點就是,經過明確地假設:數據和標題是經過這些名字儲存在槽裏這一事實,該定義將咱們與咱們的數據集的一種特定的實現拴在了一塊兒。更靈活的方法,是由數據抽象原則激發的,就是去定義一個讀取方法來獲取和修改標題和數據,好比這樣:

(defmeth x :title (&optional (title nil set))
    (if set (setf (slot-value 'title) title))
    (slot-value 'title))

還有

(defmeth x :data (&optional (data nil set))
    (if set (setf (solt-value 'data) data))
    (slot-value 'data))
那麼,咱們可使用這些讀取方法重寫:describe方法:
(defmeth x :describe (&optional (stream t))
    (let ((title (send self :title))
          (data (send self :data)))
      (format stream "This is ~a~%" title)
      (format stream "The sample mean is ~g~%" (mean data))
      (format stream "The sample standard deviation is ~g~%"
              (standard-deviation data))))
若是,過會兒咱們決定修改:title方法,也許若是槽的值是nil而咱們去提供一個有用的默認值,或者爲了確認一個槽沒有使用某個名字,這些狀況咱們就再也不須要修改:desc ribe方法,也不須要修改任何包含其它標題的方法(針對使用:title訪問消息的數據集)。

    對於每一個有它本身的方法的對小,消息:own-methods返回了一個消息列表:

> (send x :own-methods)
(:DESCRIBE :DATA :DISCRIBE)
:method-selectors消息列出了全部的在一個對象和它的祖先裏可用的方法:
> (send x :method-selectors)
(:DESCRIBE :DATA :TITLE :METHOD-SELECTORS :SLOT-NAMES :SLOT-VALUE :PRINT :RETYPE :NEW :HELP :DELETE-DOCUMENTATION :DOCUMENTATION :DOC-TOPICS :MAKE-PROTOTYPE :INTERNAL-DOC :OWN-METHODS :OWN-SLOTS :PRECEDENCE-LIST :PARENTS :ISNEW :SHOW :DELETE-METHOD :ADD-METHOD :DELETE-SLOT :ADD-SLOT :HAS-METHOD :HAS-SLOT :REPARENT :GET-METHOD)
:has-method消息與上邊描述過得:has-slot消息相同。

    你也可使用不帶參數的:delete-method消息從一個對象裏刪除一個方法,這個方法的消息選擇器符號將被刪除。

練習 6.2

略。

練習 6.3

略。

練習 6.4

略。

6.2.4 對象打印

經過向Lisp-Stat打印系統發送:print消息來打印一個對象。針對這個從根對象繼承來的消息的方法,來產生相似下邊的輸出:

#<Object: 2106610, prototype = *OBJECT*>

如下事實——以#<開始的打印的結果,表示這是一個讀取器沒法理解的形式。做爲一個替換方法,你能夠定義你本身的:print方法。爲了與這個被調用的方法的方式一致,應該加入一個可選的流參數,若是使用該流參數它將被打印到標準輸出上。舉個例子,咱們能夠爲x定義一個:print方法:

> (defmeth x :print (&optional (stream t))
    (format stream "#<~a>" (send self :title)))
:PRITN
而後,讓解釋器打印對象x產生:
> x
#<a data set>

6.3 原型和繼承

6.3.1 構造原型和實例

經過重複建立對象、添加數據和標題槽和定義一個方法集這些過程,咱們能夠定義額外的數據集對象。可是這涉及到至關大的成倍的努力。與之相反,咱們能夠重複一次這個過程來構造一個原型數據集,而後使用這個原型做爲一個模板來產生額外的數據集。

    使用defproty宏來產生一個原型,在它最簡單的形式裏,調用以下:(defproto <name> <instance slots>)。第一個參數<name>應該是一個符號,它不會被求值;第二個參數應該是這樣一個表達式:求值爲一個符號列表。defproto宏有如下幾個行爲:

  • 它構建一個新對象,分配一個全局變量<name>給對象,而後安裝帶<name>值的槽proto-name。默認地,該槽是:print方法使用的,用來構建對象的打印表達式的。
  • 它的參數列表是由<instance slots>表達式產生的表達式列表組成的,接着安裝一個名爲instance-slots的槽,在新建立的對象裏使用這個列表做爲它的值。而後,它向這個對象裏安裝一個槽,對於每個在instance-slots列表裏的符號,該槽的值爲nil。

instance-slots列表指定爲每一個由原型建立的對象指定槽名。一般地,由原型建立的不一樣的實例僅在它們的實例裏槽的值是不一樣的。咱們能夠這樣定義一個數據集原型:

> (defproto data-set-proto '(data title))
DATA-SET-PROTO
    原型對象能夠像其餘任何對象同樣使用。尤爲地,咱們能夠這樣對咱們的數據集原型的title槽賦值:
> (send data-set-proto :slot-value 'title "a data set")
"a data set"

同時,咱們也能夠用如下表達式形式爲:describe、:data、:title和:print方法賦值:

> (defmeth data-set-proto :title (&optional (title nil set))
    (if set (setf (slot-value 'title) title))
    (slot-value 'title))
:TITLE

> (defmeth data-set-proto :data(&optional (data nil set))
    (if set (setf (slot-value 'data) data))
    (slot-value 'data))
:DATA

> (defmeth data-set-proto :describe (&optional (stream t))
    (format stream "This is ~a~%" (slot-value 'title))
    (format stream "The sample mean is ~g~%"
            (mean (slot-value 'data)))
    (format stream "The sample standard deviation is ~g~%"
            (standard-deviation (slot-value 'data))))
:DESCRIBE

> (defmeth data-set-proto :print (&optional (stream t))
    (format stream "#<~a>" (send self :title)))
:PRITN

可是,一個原型的主要用途是做爲構造新對象的模板。

    原型的實例是這樣一個對象,它集成自原型幷包含原型指定的實例的槽做爲它本身的槽。能夠經過向原型發送:new消息來從一個原型構造一個實例,對於這個消息,它的方法繼承自根對象,並具備以下行爲:

  • 它建立了一個繼承自原型的新對象。
  • 它經過一個由原型的instance-slots列表指定的槽向新對象加入槽,而後將這些槽初始化爲原型裏的值。
  • 它向新對象發送帶參數的初始化消息:isnew,那些參數在調用:new消息時使用,若是有這些參數的話。
  • 它返回這個新對象。

從根對象繼承來的:isnew方法是不須要參數的,可是它容許對象裏的任意槽經過使用相應的關鍵字參數來初始化。

    經過向data-set-proto發送一個帶:data關鍵字參數(該參數用來爲data槽安置一個值)的:new消息,咱們能夠建立一個data-set-proto原型的實例:

> (setf x (send data-set-proto :new :data (chisq-rand 20 5)))
#<a data set>
新對象的打印體表示顯示了用例產生它的原型的名字。像之前同樣,咱們能夠經過向它發送:describe消息來檢測咱們的新數據集: 
> (send x :describe)
This is a data set
The sample mean is 4.886239840565315
The sample standard deviation is 3.1586394887010028
NIL

    由於任何新的數據集須要一個值做爲其數據槽,爲這個槽編寫一個:isnew方法,它須要一個爲這個槽指定的值。例如,咱們能夠這樣定義一個方法:

> (defmeth data-set-proto :isnew (data &key title)
    (send self :data data)
    (if title (send self :title title)))
:ISNEW
這容許標題以一個關鍵字參數的形式被指定。

    當建立一個對象時,經過集成得到的方法與槽集合不會被凍結。例如,建立一個繼承自data-set-proto的x對象,咱們能夠這樣爲data-set-proto定義一個:plot方法:

(defmeth data-set-proto :plot ()
    (histogram (send self :data)))
該方法也能夠被x繼承,如今想x發送:plot消息會產生它的數據的一個直方圖。

    最後,你可能想要爲咱們的原型定義一個構造函數:

(defun make-data-set (x &key (title "a data set") (print t))
    (let ((object (send data-set-proto :new x :title title)))
      (if print (send object :describe))
      object))
你也能夠編寫一個廣義函數來正式地代替消息放鬆:
> (defun describe-data (data) (send data :describe))
DESCRIBE-DATA
> (defun plot-data (data) (send data :plot))
PLOT-DATA

6.3.2 繼承自原型的原型

defproto宏可被用來設置一個原型,該原型從其餘對象,一般是從其餘原型對象繼承而來。一個對該宏的完整的調用形式爲:

(defproto <name> <instance slots> <shared slots> <parents> <doc string>)
除第一個參數外,其它參數均將被求值。最後四個參數是可選的。<instance slots>和<shared slots>這兩個參數應該是nil或者符號列表,它們的默認值是nil。<shared slots>裏的符號決定這些額外的槽將被安裝到新的原型對象裏而不是安裝在實例裏。<parents>應該是一個對象或者對象列表,一般是一個原型但不是必須的。若是<parents>參數被忽略,它的默認值爲根對象。若是使用了文檔字符串,它將被安裝用來供:help消息使用。

    若是defproto宏使用一個強制的父對象進行調用的話,那麼這個新對象將直接從<parents>繼承來進行構造。此外,被包含到新原型裏的實例槽的列表做爲如下形式存在:做爲父對象的實例槽和被指定爲<instance slots>參數的組合形式。所以,你僅須要指定你須要的全部的額外的,父對象裏不包含的那些實例槽。當defproto宏安裝父對象要求的實例槽時,它初始化這些槽到它們的被繼承值。

    讓咱們使用defproto宏爲一個時間序列對象設置一個原型,該時間序列是從咱們上邊定義過的數據集原型繼承來的。data槽和title槽將被自動包含。此外,咱們可能想要包含origin槽和spacing槽用來指定原點和觀測時間之間的間隔。這將引出以下定義:

> (defproto time-series-proto 
    '(origin spacing) () data-set-proto)
TIME-SERIES-PROTO
由於不須要共享槽,共享槽參數是一個空列表。可是爲了可以指定父原型data-set-proto,咱們不得不強制使用它。

    爲新原型準備的title槽初始化時包含從data-set-proto繼承來的值:

> (send data-set-proto :title)
"a data set"
:title方法,與data-set-proto裏的其它方法同樣,是重新的原型繼承來的。咱們能夠這樣安置一個更合適的標題:
> (send time-series-proto :title "a time series")
"a time series"
這個槽的值用來初始化時間序列實例的title槽。咱們能夠爲兩個新槽origin和spacing定義讀取函數:
> (defmeth time-series-proto :origin (&optional (origin nil set))
    (if set (setf (slot-value 'origin) origin))
    (slot-value 'origin))
:ORIGIN

> (defmeth time-series-proto :spacing (&optional (sp nil set))
    (if set (setf (slot-value 'spacing) sp))
    (slot-value 'spacing))
:SPACING
而後,這些槽的默認值能夠這樣安置:
> (send time-series-proto :origin 0)
0
> (send time-series-proto :spacing 1)
1

    如今咱們能夠這樣構造一個時間序列對象來表示一個短的移動的平均值序列:

> (let* ((e (normal-rand 21))
        (data (+ (select e (iseq 1 20))
                 (* .6 (select e (iseq 0 19))))))
    (setf y (send time-series-proto :new data)))
#<a time series>

從data-set-proto原型繼承來的:isnew方法須要data參數。若是咱們像該新時間序列發送:describe消息,從data-set-proto原型繼承來的方法將產生:

> (send y :describe)
This is a time series
The sample mean is 0.4011466522084276
The sample standard deviation is 0.9777933783149011
NIL

6.3.3 覆寫和修改繼承的方法

從data-set-proto繼承來的:plot方法產生了一個時間序列直方圖。它更合適去繪製相對於時間的序列。咱們能夠在時間序列原型裏爲:plot定義一個新方法,而後"覆蓋"繼承來的:plot方法,就像這樣:

> (defmeth time-series-proto :plot ()
    (let* ((data (send self :data))
           (time (+ (send self :origin)
                    (* (iseq (length data))
                       (send self :spacing)))))
      (plot-points time data)))
:PLOT
    由繼承來的:describe方法產生的概述是不合理的,可是若是想該方法里加入一些適合時間序列的統計方法,好比說自相關函數,那就完美了。爲了向:describe打印的信息里加入一個自相關函數,咱們可使用一個新的方法,該方法編寫了一個信息分支,覆寫從data-set-proto繼承來的方法。可是,若是可以調用繼承來的方法,而後加入新代碼行,那就更容易了。從一個覆蓋方法的定義裏調用其繼承方法的方式,就是使用call-next-method函數。該函數會到優先級列表裏查找與調用的方法名字相同的下一個方法,在擁有該方法的那個對象執行後開始進行搜索。而後它使用那個方法到當前對象,並使用提供給它的任何附加參數。針對這個時間序列的例子,假設咱們有一個叫作autocorrelation的函數,咱們能夠這樣定義一個新的:display方法:
> (defmeth time-series-proto :describe (&optional (stream t))
    (call-next-method stream)
    (format stream
            "The autocorrelation is ~g~%"
            (autocorrelation (send self :data))))
:DESCRIBE
autocorrelation函數能夠這樣定義:
> (defun autocorrelation (x)
    (let ((n (length x))
          (x (- x (mean x))))
      (/ (mean (* (select x (iseq 0 (- n 2)))
                  (select x (iseq 1 (- n 1)))))
         (mean (* x x)))))
AUTOCORRELATION
使用這個定義,發送:display方法給移動的平均時間序列y的結果是:
> (send y :describe)
This is a time series
The sample mean is 0.4011466522084276
The sample standard deviation is 0.9777933783149011
The autocorrelation is 0.40181991922789645
NIL
函數call-next-method也能夠用來產生一個:isnew方法的定義的替換函數,該方法是針對數據集原型的:
> (defmeth data-set-proto :isnew (data &rest args)
    (apply #'call-next-method :data data args))
:ISNEW
該定義利用了以下事實,即下一個:isnew方法,針對根對象的方法,容許使用關鍵字參數來初始化槽。提供一個使用:title關鍵字的標題仍然是可能的,由於超出須要的data參數以外的任何參數都會被傳送給下一個:isnew方法。

    此時咱們可能想要調用屬於一個對象的方法,該方法當前對象的繼承路徑裏,或者是一個不在優先級列表裏的下一個方法。咱們可使用call-method函數來達到這一目的。該函數這樣調用:
(call-method <owner> <selector> <arg 1> ... <arg n>)
<owner>的優先級列表的搜索是爲消息<selector>查找方法,而後那個方法將使用當前對象和全部額外測參數。使用call-method函數,time-series-proto原型的:describe方法能夠這樣定義:

> (defmeth time-series-proto :describe (&optional (stream t))
    (call-method data-sset-proto :describe stream)
    (format stream
            "The autocorrelation is ~g~%"
            (autocorrelation (send self :data))))
:DESCRIBE
call-method和call-next-method方法,它們只能在一個方法定義體內使用。

    對象系統不容許你從公共區域訪問想要內部使用的槽和消息。爲了飽和你本身免受被繼承的方法的破壞,你應該確保不要去修改槽或者覆寫方法,除非你肯定你的修改與它們被其它方法使用的結果是一致的。再強調一次,不管什麼時候,避免直接使用讀取方法讀取槽十個好主意。

練習 6.5

略。

練習 6.6

略。

6.4 額外的細節

本節展現Lisp-Stat對象系統的一些低層面的細節,首次接觸者能夠忽略本節的閱讀。

6.4.1 建立對象和原型

你可使用make-object函數構建一個對象,代替使用一個原型來構建。傳遞給make-object函數的參數是這個新對象的父對象。若是不適用參數調用make-object函數,那麼它將構建一個繼承自根對象的對象。所以,第6.2.2節的數據集對象能夠這樣構建:

> (setf x (make-object))
#<Object: 13de8e4, prototype = *OBJECT*>
由make-object構造的對象是不包含槽的,即便他們是經過對一個原型對象繼承而構造的。這類對象須要的槽不得不使用:add-slot消息強制添加。

    原型就是一個包含proto-name和instance-slots兩個槽的對象。構建一個原型最簡單的方式就是使用defproto宏。可是你也可使用一個現存的對象構建,好比說上邊構建的對象x,而後經過發送:make-prototype消息將它轉換爲一個原型。該消息帶兩個參數,一個用來做爲proto-name槽的值的符號和一個表示額外實例變量的符號列表。該消息的方法將查找由對象的父對象指定的實例槽,若是有的話,使用消息裏指定的實例槽將它們組合在一塊兒,而後將結果安置在instance-slots槽裏。而後,它爲instance-slots列表裏的每個元素安置一個槽,除非這樣的槽已經存在了。如今槽已經使用從對象的祖先繼承來的值初始化過了。所以,咱們可使用如下表達式將咱們的x對象轉換爲一個原型:

> (send x :make-prototype 'data-set-proto '(data title))
#<Object: 13de8e4, prototype = DATA-SET-PROTO>
這個方法在原型的交互式開發中多是有用的。它也能夠用來構建臨時原型,即在一個函數體裏或者let表達式裏使用的原型,可是不須要一個全局原型的肯定性。

    Lisp-Stat原型系統是經過defproto宏和針對:new和:make-prototype消息的根對象方法來實現的。這些方法不該被覆寫,除非你想修改原型系統。

6.4.2 額外的方法和函數

爲了檢測對象,有一些額外的方法和函數時可用的。

    經過使用:precedence-list消息,對象的優先級列表能夠包含到對象裏,它的直接父對象能夠經過使用:parents來包含。例如,對於第6.3節的那個原型:

> (send time-series-proto :precedence-list)
(#<a time series> 
#<a data set> 
#<Object: 13fd0c8, prototype = *OBJECT*>)
> (send time-series-proto :parents)
(#<a data set>)
    謂詞kind-of-p能夠用來確認一個對象是否繼承自另外一個對象。例如,由於time-series-proto繼承自data-set-proto:
> (kind-of-p time-series-proto data-set-proto)
T
> (kind-of-p data-set-proto time-series-proto)
NIL
    經過向一個對象發送參數爲新父對象或多個新對象的:reparent消息,改變該對象的繼承關係是可能的。例如,咱們可使用下邊的表達式讓咱們的數據集x從time-series-proto原型繼承:
> (send x :reparent time-series-proto)
#<NIL>
對一個對象從新定義父對象不會影響做爲它後代的任何一個對象的優先級列表。本書撰寫的時候,從新定義一個對象的父對象的能力仍是一個試驗工具。該工具應該謹慎使用,由於除非經過他的新的優先級列表裏的方法,不然沒有辦法保證該對象有這個槽。

6.4.3 共享槽

defproto宏容許共享槽的設置,該槽被安置在原型裏而不是在實例裏複製。這個機制不是很經常使用,可是偶爾頗有用。舉個例子,假設你想可以回溯一個特殊原型的全部實例,你能夠像這樣定義原型來達到目的:

> (defproto myproto '(...) '(instances))
而後爲:new消息定義一個方法:
> (defmeth myproto :new (&rest args)
    (let ((object (apply #'call-next-method args)))
      (setf (slot-value 'instances)
            (cons object (slot-value 'instances)))
      object))
:NEW
由於這是對原型機制的修改,因此本例中對根對象的:new方法的覆寫是合理的。

    在這一點上忠告多是合適的。日常,若是建立了一個對象,他就會一直存在,只要它做爲一些變量的值或者一些結構的組件。一旦它不被引用了,Lisp系統就會在垃圾回收進程裏自動回收它的空間。你無需強制釋放。然而,若是一個原型要回溯它的實例,那麼該實例就會從原型裏被引用,而且不會被回收。所以你將不得不經過定義一個:dispost方法,自行安排去強制性地通知原型有一個對象再也不須要了:

> (defmeth myproto :dispose (object)
    (setf (slot-value 'instances)
          (remove object (slot-value 'instances))))
:DISPOSE
在像一個將對象做爲參數的原型發送了:dispost消息以後,該對象再也不被原型引用,若是也沒其它引用的話該對象將被垃圾回收。:dispose消息必須傳遞給原型而不是對象,由於它要可以修改原型的instances槽。

6.4.4 對象文檔系統

對象系統容許你爲你選擇的對象的任何主題安置文檔字符串。這些主題能夠被符號名識別,也能夠經過使用:documentatiion消息來設置和取回。例如,

(send data-set-proto :documentation :title)
上邊的表達式返回了主題:title的文檔字符串,若是它有文檔字符串的話。若是該主題沒有文檔字符串,消息返回nil。消息選擇器關鍵字的文檔字符串一般經過使用defmeth宏來安置,可是也能夠經過使用:documentation消息來安置。
> (send data-set-proto :documentation
        :title "sets or returns the title")
"sets or returns the title"
上邊的表達式爲:title安置了文檔字符串。當須要對:title的幫助的時候,該字符串能夠被:help消息使用:
> (send data-set-proto :help :title)
TITLE
sets or returns the title
NIL
    文檔字符串儲存在documentation槽裏。當爲一個主題取回文檔的時候,:documentation方法會搜索對象優先級列表裏的全部的documentation槽。當要求安置一個新的文檔字符串的時候,在對象裏接收消息的方法會安置它。若是這個對象沒有documentation槽,將會建立一個。爲了避免妨礙系統運行,你不該該直接修改一個對象的documentation槽的值。

    :doc-topics消息返回將爲全部可用的文檔字符串返回符號列表。針對數據集原型,發送這個消息的結果是:

> (send data-set-proto :doc-topics)
(:TITLE :REPARENT :INTERNAL-DOC :OWN-METHODS :OWN-SLOTS :PRECEDENCE-LIST :PARENTS :ISNEW :SHOW :DELETE-METHOD :DELETE-SLOT :ADD-METHOD :ADD-SLOT :HAS-METHOD :HAS-SLOT :GET-METHOD PROTO :METHOD-SELECTORS :SLOT-NAMES :SLOT-VALUE :PRINT :RETYPE :NEW :HELP :DELETE-DOCUMENTATION :DOCUMENTATION :DOC-TOPICS)
在使用:help消息時,若是沒有爲它提供參數,那麼:help消息將使用:doc-topics消息:
> (send data-set-proto :help)
DATA-SET-PROTO
The root object.
Help is available on the following:

ADD-METHOD ADD-SLOT DELETE-DOCUMENTATION DELETE-METHOD DELETE-SLOT DOC-TOPICS DOCUMENTATION GET-METHOD HAS-METHOD HAS-SLOT HELP INTERNAL-DOC ISNEW METHOD-SELECTORS NEW OWN-METHODS OWN-SLOTS PARENTS PRECEDENCE-LIST PRINT PROTO REPARENT RETYPE SHOW SLOT-NAMES SLOT-VALUE TITLE 
NIL
該條幫助信息的頭部是基於proto主題的文檔的。若是defproto給定了一個文檔字符串,那麼那個字符串將安置在proto主題下。若是咱們直接安置一個文檔字符串,例如,像這樣:
> (send data-set-proto :documentation
        'proto "A generic data set.")
"A generic data set."
那麼:help消息產生的頭部將更加合理:
> (send data-set-proto :help)
DATA-SET-PROTO
A generic data set.
Help is available on the following:

ADD-METHOD ADD-SLOT DELETE-DOCUMENTATION DELETE-METHOD DELETE-SLOT DOC-TOPICS DOCUMENTATION GET-METHOD HAS-METHOD HAS-SLOT HELP INTERNAL-DOC ISNEW METHOD-SELECTORS NEW OWN-METHODS OWN-SLOTS PARENTS PRECEDENCE-LIST PRINT PROTO REPARENT RETYPE SHOW SLOT-NAMES SLOT-VALUE TITLE 
NIL
    :delete-documentation消息帶一個參數,是一個主題符號,若是一個主題有文檔字符串的話,將從文檔裏爲該主題刪除文檔字符串。

6.4.5 保存對象

爲了可以在另外一個會話或者機器裏恢復對象,有時將一個對象保存到文件裏是有用的。開發一個對全部的可能的對象都起做用的策略看起來是不可能的。所以Lisp-Stat採用了這樣一個慣例:一個能被保存到文件的對象應該相應:save消息。該消息應該返回一個能夠被打印到一個文件的表達式。當該表達式被讀回和求值的時候,能夠構造出對象的複製品。例如,對於data-set-proto原型,可使用反引號機制定義一個:save方法:

> (defmeth data-set-proto :save ()
    `(send data-set-proto :new
           ',(send self :data)
           :title ',(send self :title)))
:SAVE
向data-set-proto的一個實例發送這個消息的結果以下:
> (send x :save)
(SEND DATA-SET-PROTO :NEW 
      (QUOTE NIL) 
      :TITLE (QUOTE NIL))
上邊的結果表達式已經被編輯過以更有可讀性。

    savevar函數使用:save消息保存變量,其值是要保存到文件的對象。一些對象沒有提供:save方法,試圖保存這樣的對象將引起一個錯誤。

6.5 一些內置原型

Lisp-Stat包含一些內置原型。這些原型的繼承樹的一個子集顯示在圖6.1中。這些原型裏的不少都與系統的繪圖部分是相關聯的,將在下一章裏描述。本節只大略地表示非繪圖原型中的三個:組合數據原型,迴歸模型原型和非線性迴歸模型原型。

6.5.1 組合數據對象

到目前爲止,咱們僅僅考慮了列表和數組做爲可能的組合數據項。更多通常的集合也能夠定義爲組合數據對象。設計矢量化算術系統和map-elements函數時也使用了這些對象。任何一個繼承自compound-data-proto原型的對象都是組合數據對象,並具備以下特徵:

  • 它包含一個元素的序列,這些元素能夠經過使用:data-seq消息來提取。
  • 它的數據序列的長度可使用:data-length消息來確認。
  • 統統過使用:make-data消息,它能夠建立一個像它自身同樣的新對象,除了一個新的數據序列。該消息的參數能夠是一個列表或者一個向量。

    舉個簡單的例子,讓咱們定義一個對象來表示二維空間裏的一個點,該對象帶兩個實例槽,它的笛卡爾座標x和y:

> (defproto point-proto '(x y) '() compound-data-proto)
POINT-PROTO
咱們能夠像這樣定義方法來得到槽:
> (defmeth point-proto :x (&optional (x nil set))
    (if set (setf (slot-value 'x) x))
    (slot-value 'x))
:X

> (defmeth point-proto :y (&optional (y nil set))
    (if set (setf (slot-value 'y) y))
    (slot-value 'y))
:Y
而後,初始化方法能夠這樣定義:
> (defmeth point-proto :isnew (x y)
    (send self :x x)
    (send self :y y))
:ISNEW
    對於這三個必需的方法,它們的定義是:
> (defmeth point-proto :data-length () 2)
:DATA-LENGTH
> (defmeth point-proto :data-seq ()
    (list (send self :x) (send self :y)))
:DATA-SEQ
> (defmeth point-proto :make-data (seq)
    (send point-proto :new (select seq 0) (select seq 1)))
:MAKE-DATA
咱們也能夠定義一個顯示兩個座標的:print方法:
> (defmeth point-proto :print (&optional (stream t))
    (format stream
            "#<a point located at x = ~d and y = ~d>"
            (send self :x)
            (send self :y)))
:PRINT
如今咱們能夠構造一個樣本點a:

    經過向矢量化算術系統傳送:data-seq消息,該系統能夠提取一個組合數據對象的數據序列。若是傳遞給矢量化函數的第一個複合參數是一個組合數據對象的話,那麼經過向那個對象傳送:make-data消息能夠構造結果。由於咱們的點對象遵循必要的協議,例如,這意味着咱們可使用+函數來加兩個點,使用*函數讓一個點與一個常量相乘,或者使用log函數求取一個點的座標的對數值:

> (+ a a)
#<a point located at x = 6 and y = 10>
> (* a 2)
#<a point located at x = 6 and y = 10>
> (log a)
#<a point located at x = 1.0986122886681098 and y = 1.6094379124341003>
    肯定結果的類型的慣例是基於矢量化函數調用的第一個複合數據參數的,這個慣例固然是人爲規定的,可是它通常會產生合理的結果。

練習 6.7

6.5.2 線性迴歸模型

線性迴歸模型對象已經在第2.6節裏介紹過了。本小節將給出額外的細節。

    迴歸模型原型是regression-mode-proto,它可使用一個像下式的表達式來構建:

> (defproto regression-mode-proto
    '(x y intercept weights included
        predictor-names response-name case-labels
        sweep-matrix basis
        total-sum-of-squares
        residual-sum-of-squares))
REGRESSION-MODE-PROTO

所以這個原型有12個實例槽,沒有共享槽,繼承自根對象。前8個槽包含描述模型的信息。每個槽均可以經過對應的關鍵字:x, :y, :intercept等等命名讀取方法來進行讀取和修改。改變前5個槽中的任何一個都須要被計算的模型對象內的估計,從新計算髮生在下一次須要計算結果的時候。最後四個槽是計算方法內部使用的。相應的關鍵字命名的讀取方法能夠用來讀取槽的內容,可是沒法改變它們的值。槽讀取器和讀取方法是惟一的能夠直接使用槽的方法;其它的方法只能使用讀取函數和讀取方法來使用槽。

    迴歸模型原型裏的剩餘的方法能夠被分紅幾個組。對應:df, :num-cases, :num-coefs和:num-included的方法將返回關於模型維度的信息。:sum-of-squares方法將返回殘差平方和。對應:x-matrix的方法,若是其中包括截距這一術語的話,它將返回一個列矩陣,其後跟着一個對應擬合過程裏的一個基礎應用的獨立變量。帶權值狀況下的(XTX)-1(XTWX)-1矩陣是由:xtxinv返回的,這裏的X是由:x-matrix返回的矩陣。擬合過程當中使用的列的下標列表是由:basis消息返回的。

    係數的標準差和估計量是由coef-standard-errors和:coef-estimates返回的。:sigma-hat返回估計後的均方差。擬合後的值由:fit-values返回,採樣平方與相關性係數由:r-squared返回。

    對殘差和影響性分析有用的消息有:cooks-distance, :externally-studentized-residuals, :leverages, :raw-residuals, :residuals和:studentized-residuals。還有兩個殘差繪圖方法是:plot-bayes-residuals和:plot-residuals。

    模型的概略性描述能夠由:display消息打印。regression-model函數將發送這個消息到一個新模型,若是該函數的值不是nil的話。針對:save消息的方法將試圖返回一個表達式,該表達式能夠求值而從新生成模型對象。

    :compute和:need-computing主要是內部使用。:compute方法從新計算估計量。:needs-compute的方法若是以不帶參數的形式調用將肯定該模型是否須要被從新計算;若是以帶參數t的形式調用,在被計算的槽下次被讀取的時候,它將告訴對象從新計算自身。這容許模型的一些特性能夠馬上被修改而不須要每次修改以後都進行模型的從新計算。

    :isnew方法僅會設置模型對象,而後將它標記爲須要被從新計算。它不容許經過關鍵字參數來指定槽。所以若是你不想使用regression-model構造函數構建一個模型,你能夠向原型發送:new消息來構造一個新的對象,而後使用讀取方法向模型裏添加模型組件。

6.5.3 非線性迴歸模型

經過下邊的表達式,能夠定義非線性迴歸原型nreg-model-proto:

> (defproto nreg-model-proto
    '(mean-function theta-hat epsilon count-limit verbose)
    nil
    regression-model-proto)
NREG-MODEL-PROTO
所以,它繼承自regression-model-proto,而且有五個額外的實例槽。槽mean-function處理模型的平均值函數,theta-hat包含參數的估計量,剩餘的槽是針對這些估計量,用來控制估計量的迭代計算處理的。theta-hat槽也以被對應的關鍵字命名的讀取器方法讀取。剩餘的方法能夠經過使用讀取方法讀取和修改。使用讀取函數來改變這些槽的任何一個都會將對象標記爲須要從新計算的對象。

    使非線性迴歸模型原型繼承線性模型原型的方法看起來很是的奇怪。非線性即不必定是線性的意思,非線性迴歸模型是線性迴歸模型的一個超集,在目前給定的全部例子裏,新的原型已經做爲它們的父對象的特化被介紹了。在這種狀況下我將使用繼承關係來表示一種類似性而非特化性。擬合和計算擬合值這兩個操做在非線性模型和在非線性模型裏是很是不一樣的。可是估計量的標準偏差、槓桿值(即用於模型穩定性評價的值)和類似量一般是經過對非線性模型的線性化的方式計算出來的,該非線性模型是使用被估計參數值的必定範圍的擴大值獲得的。在這個線性化的近似值中,X矩陣的角色由被估計參數值處的均值函數的雅克比矩陣充當。有了這個身份,標準差、槓桿值等等的公式,還有對於線性狀況在非線性的狀況下也有意義,至少做爲一階近似值是有意義的。

    這種類似性是由繼承關係捕捉的。非線性模型的原型包含一個針對:compute的新方法,該方法可使用帶回溯的高斯-牛頓搜索法找出極大似然估計量。而後它將雅克比矩陣安置爲x槽的值,對於現行迴歸計算使用線性估計值,再利用繼承來的:compute方法將它添加到槽中。最後,它將剩餘平方和的合適的值安置到它的槽裏。也能夠爲:fit-values、:coef-estimates和:save定義新的方法。一個叫作:parameter-names的新消息能夠加到對象裏來爲顯示方法而提供它們的名字。這些儲存在predictor-names槽裏。由於改變X矩陣, 截距, 或者謂詞名再也不有意義,對應的消息會被新的方法覆寫,這些方法僅容許讀取,不容許修改。最後,爲:new-initial-guess準備的一個方法被添加到對象裏,目的是容許交互性地搜索估計值,而後在一個新的初始化值處重啓。全部其它的方法都是繼承自線性迴歸原型的。它們提供了基於線性化模型的合理的估計值。例如,若是找到了槓桿值的一個較好的定義,那麼繼承來的方法可使用一個針對非線性模型的一個方法來覆寫。可是到目前爲止,一階近似值是自動可用的。

    非線性模型也能夠定義爲線性模型的父級。存在正態線性迴歸模型的其它可能的泛化,它能夠被安置到比線性化模型提早發生的位置。其中的一個例子就是文獻42中的McCullagh和Nelder表述的真正的泛化的線性模型。它們均可以使用多重繼承方法做爲線性模型的父級,就像下一節要描述到的同樣。哪種方法更好還不肯定,這兩種方法分別是經過類似性使用推理的方法和經過泛化而基於一個排序的方法。這兩種方法都有各自的優點。這裏使用的方法可能更加適合這種狀況:在該狀況裏模型的新類型正在被開發,而且須要從繼承關係裏獲益而使開發變成更簡單和更易理解的形式。換句話說,泛化的順序頗有可能更適合這種狀況:即在這種狀況下層級關係容易理解,而且易於進一步的開發。儘管正態非線性迴歸模型已經被普遍研究,在方法論上仍然有至關大的空間改進該理論以適應對它的分析。這也就是我選擇在這裏的描述表達方式的緣由。

6.6 多重繼承

目前用到的涉及到對象的全部例子都是單獨父級的繼承。所以,這些對象的優先級列表能夠經過簡單回溯的方法來肯定,回溯過程是從該對象的父級開始一直到根對象的過程。在多重繼承裏(即一個對象有多個父級的狀況),再也不有一個構建優先級列表的明顯的方式,咱們須要採用一些慣例。

    優先級列表就是設計用來提供對象及其祖先的完整的順序的列表。該順序使用以下規則構建:

  • 對象老是先於它的父級。
  • 對象的父級的列表提供一個關於這些對象的局部的順序,這個順序必須是能夠保存的。
  • 重複的對象將從順序裏忽略掉,出現次數超過一次的對象將在順序上儘量地放在開始的位置,而不是違反其它規則。
  • 若是沒有這樣的規則存在則發生錯誤。

    這裏有一個關於一個原型集合順序產生的例子,定義6個原型:

> (defproto food)
FOOD
> (defproto spice () () food)
SPICE
> (defproto fruit () () food)
FRUIT
> (defproto cinnamon () () spice)
CINNAMON
> (defproto apple () () fruit)
APPLE
> (defproto pie () () (list apple cinnamon))
PIE
那麼pie的優先級列表就是:
> (send pie :precedence-list)
(#<Object: 142dea8, prototype = PIE> 
#<Object: 141e78c, prototype = APPLE> 
#<Object: 141eb9c, prototype = FRUIT> 
#<Object: 141e97c, prototype = CINNAMON> 
#<Object: 141ed4c, prototype = SPICE> 
#<Object: 141f08c, prototype = FOOD> 
#<Object: 13fd510, prototype = *OBJECT*>)
    不是全部的父級列表都是一向的順序,例如:
> (defproto spice-cake () () (list spice pie))
Error: inconsistent precedence order
Happened in: #<FSubr-DEFPROTO: #13feb50>
以上定義須要spice在優先級列表中先於pie,而現存的定義須要相反的順序。在spice-cake中使用反向順序就會起做用了:
> (defproto spice-cake () () (list pie spice))
SPICE-CAKE
    發揮多重繼承的一個有用的方式是建立一個具備多個原型的庫,每一個原型都包含必定的特性,好比說打印或跟蹤標籤集的能力。那麼新的須要這些特性的原型就能夠建立爲表示它們須要的這些特性的組合對象的後代。咱們建立的爲了與其它原型混合在一塊兒的原型叫作mixins,Mixins將用來實現 第10.4節中的盛大旅行(grand tours).

6.7 其它對象系統

面向對象編程的起源一般要追溯到1960年代後期,1970年代前期由Simula開發的Simulation語言。這種編程範式最先發現是在Smalltalk語言中使用的,Smalltalk語言是施樂公司帕洛阿爾託研究中心在1970年代中葉開發的。Smalltalk是純面嚮對象語言:全部的數據都是對象。在Smalltalk裏像 1+2這樣的表達式將被解釋爲」向對象1發送消息,將對象2加到它之上,而後返回一個表示結果的對象。「

    在堅持使用面向對象的編程範式的力度方面,Smalltalk是不一樣尋常的。大多數系統支持面向對象編程都是使用一個混合的方法,即向已存在的語言里加入面向對象編程工具,這樣的系統的例子有C++和Objective-C,還有Pascal的Classcal和Object等Pascal擴展。針對Lisp的面向對象擴展也已經開發出來了,包括Object Lisp,Flavors,Common Objects和Common Lisp對象系統CLOS。

    最近幾年面向對象思想已經受到至關的關注,由於他們在設計圖形用戶界面方面的使用時很理想的,就像那些在Smalltalk系統裏的部分和由Apple Macintosh系統推廣中開發的接口。繪圖對象,好比窗體和菜單,它們能夠很天然地表示成軟件對象,用戶所作的操做,好比說要求一個窗體改變大小或者按下一個按鈕來初始化一個動做,這些操做很天然地被翻譯到消息裏再發送給這些對象。上邊提到的兩個面向對象的Pascal方言都是主要爲了支持Macintosh操做系統的編程而開發的。

    大多數可編譯的對象系統都是基於類思想的。類是一個泛化的模板,或者說是一個對象的定義,很像C語言裏的結構體的定義,或者Pascal裏記錄的定義。特殊的是,一個類一般不是一個對象。對象是做爲一個特定類的實例來構建的。類的定義要指定槽,或者叫實例變量,那是它的實例應給有的,它還應該指定一個或多個類做爲其超類。所以爲了構造一個帶有指定槽集合的對象,你必須首先定義一個合適的類,而後構造一個對象做爲該類的實例。在編譯系統裏,好比C++或者Object Pascal,類在程序源碼裏定義,一旦程序被編譯,類的定義將固定。相反,類的實例能夠在運行時動態分配。Smalltalk和基於CLOS的對象系統的標準類是基於類的系統。

    對基於類的方法的替換方案就是容許任意對象直接繼承任何其它對象,就像集成在Lisp-Stat裏的系統同樣。相似系統的其它例子包括Object Lisp系統和Trellis/Owl語言。這些系統一般叫作基於原型的系統。原型,也叫作exemplars,能夠像類同樣使用來幫助組成一個繼承層級關係,可是它們的使用不是強制性的。這就賦予了基於原型的系統更多的靈活性。這個靈活性的代價就是一個類標識符不能用來提供類型檢查信息,也不能自動地確認一個對象是否有針對指定方法須要的合適的槽集合。由於槽是不能自動地加入到對象的,對於一個編譯器想在編譯器優化方法就更困難了。

    關於基於類仍是基於原型或者基於範例的相對優勢的談論一直在面向對象編程文化中進行着。已經表達的一個觀點是基於類的方法可能更加適合良好理解和成熟程序的實現和維護,而基於原型的方法可能更適合程序的開發階段。由於交互式的統計計算在不少方面是一個實驗性的編程形式,我相信這個論點有助於使用基於原型的方法做爲一個統計系統的一部分。在任什麼時候候均可以向對象增長槽的能力,這與向S結構裏增長組件的能力是類似的,這個在S系統裏已經證實是很是有用的。由於這些緣由我選擇開發一個基於原型的系統來供Lisp-Stat使用。

    Common Lisp社區最近開發了一個針對面向對象編程的標準叫作Common Lisp對象系統,即CLOS。該系統提供了一個標準的基於類的對象系統,並將多重繼承哈多方法合併到CLOS中,目的是分發多個參數。它也提供了一個元類協議用來實現可替換的對象系統。Lisp-Stat系統在很大意義上與標準CLOS系統不一樣,可是它能夠在由元類協議提供的框架內部實現。

6.8 一些例子

6.8.1 一個矩形數據集

在相關性分析中使用的數據集一般被視爲一個表或者一個矩陣,案例用行來表示,變量用列表示。咱們能夠爲這個數據類型增長一個數據集原型,這個數據類型經過將每一個變量表示爲一個列表,看作是一個帶標籤字符串的"面向列"的視圖。另外,咱們也能夠容許在圖形裏使用案例標籤。該原型能夠定義爲:

> (defproto rect-data-proto '(vlabels clabels) () data-set-proto)
RECT-DATA-PROTO
槽vlabels和clabels分別表明變量和案例標籤。

    初始方法安置了data,title和一個使用標籤的集合。若是沒有使用任何標籤,將使用變量和案例指標構建摸女人的標籤:

> (defmeth rect-data-proto :isnew
    (data &key title variable-labels case-labels)
    (let ((n (length (first data)))
          (m (length data)))
      (send self :data data)
      (if title (send self :title title))
      (send self :variable-labels
            (if variable-labels
                variable-labels
                (mapcar #'(lambda (x) (format nil "X~a" x))
                        (iseq 0 (- m 1)))))
      (send self :case-labels
            (if case-labels
                case-labels
                (mapcar #'(lambda (x) (format nil "~a" x))
                        (iseq 0 (-n 1)))))))
:ISNEW
這個方法應該被修改來檢查全部的數據列表是不是等長度的。

    咱們須要針對:cariable-labels和:case-labels消息的方法,用來設置和得到標籤:

> (defmeth rect-data-proto :variable-labels
    (&optional (labels nil set))
    (if set (setf (slot-value 'vlabels) labels))
    (slot-value 'vlabels))
:VARIABLE-LABELS
> (defmeth rect-data-proto :case-labels
    (&optional (labels nil set))
    (if set (setf (slot-value 'clabels) labels))
    (slot-value 'clabels))
:CASE-LABELS
而後咱們能夠給原型一個新的標題,做爲實例的新的默認標題:

> (send rect-data-proto :title "a rectangular data set")
"a rectangular data set"
做爲一個測試用的例子,咱們能夠在使用如下在第5.6.2節裏介紹過的 煙道損失數據(stack loss data):

(setf stack
        (send rect-data-proto :new (list air temp conc loss)
              :variable-labels
              (list "Air" "Temp." "Concent." "Loss")))
    繼承來的槽的方法不是很合適,咱們應該根據數據集裏的變量的數目來使用不一樣的槽:

> (defmeth rect-data-proto :plot ()
    (let ((vars (send self :data))
          (labels (send self :variable-labels)))
      (case (length vars)
        (1 (histogram vars :variable-labels labels))
        (2 (plot-points vars :variable-labels labels))
        (3 (spin-plot vars :variable-labels labels))
        (t (scatterplot-matrix vars :variable-labels labels)))))
:PLOT
    繼承來的:describe方法仍然有做用,可是不是頗有用:

> (send stack :describe)
This is a rectangular data set
The sample mean is 46.3333
The sample standard deviation is 29.6613
練習 6.8

略。

練習 6.9

略。

6.8.2 一個備用的數據表示

表示矩形數據的另外一個方法,是由McDonald提倡的,即經過表示每個觀測值或者案例,將一個"面向行"的視圖做爲對象。每一個案例都給定槽,用來處理基礎案例數據,還要有方法用來提取信息。這些方法被用來表示變量。表示派生的變量的其它的方法也被定義到這些基本方法裏。

    爲了開始,一個觀測量原型能夠定義成以下樣子:

> (defproto observation-proto '(label))
OBSERVATION-PROTO
全部的觀測值應該至少包含一個觀測值標籤。爲確保這一點,咱們能夠在初始化方法裏使用gensym函數。它構建一個符號,它的名字由後邊跟着一個整數的字符串指定,該整數在每次調用gensym時都會遞增。函數string將這個符號做爲參數,而且返回它的打印體名字:

> (defmeth observation-proto :isnew (&rest args)
    (setf (slot-value 'label) (string (gensym "GBS-")))
    (apply #'call-next-method args))
:ISNEW

若是咱們假設這裏的標籤和一個觀測量可能擁有的全部的其它槽,它們僅當在一個對象構建的時候使用:isnew方法來填充,那麼咱們僅須要讀取槽的方法。例如,一個讀取標籤的方法這樣給定:

> (defmeth observation-proto :label () (slot-value 'label))
:LABEL
爲了使用這個方法來檢測煙道損失數據集,咱們能夠爲那個數據集定義一個原型觀測量,目的就是讓表明這四個值的槽在每個觀測日都被測量:
> (defproto stack-obs-proto
    '(air temp conc loss) () observation-proto)
STACK-OBS-PROTO
這四個槽的讀取方法能夠定義成這樣:
> (defmeth stack-obs-proto :air () (slot-value 'air))
:AIR
> (defmeth stack-obs-proto :temp () (slot-value 'temp))
:TEMP
> (defmeth stack-obs-proto :conc () (slot-value 'conc))
:CONC
> (defmeth stack-obs-proto :loss () (slot-value 'loss))
:LOSS
    這個方法的一個優點就是咱們能夠對任意觀測量添加額外的信息。若是一個觀測量剛好在不利的條件下已經被記錄了,那麼包含對這種影響的備註信息的槽就能夠添加進來了。若是咱們將咱們的變量記錄爲數字列表的形式,或者咱們將數據組合到一個矩陣裏,這就會變得更加困難。另外一個優點就是派生的變量能夠構形成與基本變量相同的表現,好比由消息:air,:temp,:conc,:loss表示的那些基礎變量。例如,表示損失數據的對數值的變量能夠定義成這樣:
> (defmeth stack-obs-proto :log-loss () (log (send self :loss)))
:LOG-LOSS
    一旦咱們有一個觀測量對象的列表,咱們就能夠放置到一個數據集對象裏作進一步的檢測。若是咱們爲光廁紙列表定義這個數據集來包含一個槽:
> (defproto data-set-proto '(data))
DATA-SET-PROTO

那麼咱們能夠定義一個方法,爲每個觀測量獲取一個變量的值得列表:

> (defmeth data-set-proto :values (var)
    (mapcar #'(lambda (x) (send x var))
            (slot-value 'data)))
:VALUES
使用該方法,咱們能夠定義一個:plot方法來帶一個或多個變量消息關鍵字爲參數,爲變量找出值,針對指定的變量的數目使用看起來最合適的繪圖方法來繪製這些值:
> (defmeth data-set-proto :plot (&rest args)
    (let ((vals (mapcar #'(lambda (var) (send self :values var))
                        args))
          (labels (send self :values :label))
          (vlabs (mapcar #'string args)))
      (case (length args)
        (0 (error "too few arguments"))
        (1 (histogram vals
                      :point-labels labels
                      :variable-labels vlabs))
        (2 (plot-points vals
                        :point-labels labels
                        :variable-labels vlabs))
        (3 (spin-plot vals
                      :point-labels labels
                      :variable-labels vlabs))
        (t (scatterplot-matrix vals
                               :point-labels labels
                               :variable-labels vlabs)))))
:PLOT
由於見傳遞給:plot做爲消息關鍵字,它們是引用儲存在槽裏的數據仍是表示派生的數據,這沒有什麼不一樣。

    定義一個描述數據集裏全部變量的:describe方法再也不可能,由於這個再也不是一個有意義的概念:觀測值能夠包含一個僅有不多共性的的各類不痛的變量。相反地,咱們可讓:describe帶一個或多個變量消息關鍵字做爲參數,再提供一個這些變量的合理的概述信息。僅帶一個變量的一個簡單的方法能夠定義成這樣:

> (defmeth data-set-proto :describe (var)
    (let ((vals (send self :values var)))
      (format t "Variable Name: ~a~%" var)
      (format t "Mean: ~g~%" (mean vals))
      (format t "Median: ~g~%" (median vals))
      (format t "Standard Deviation: ~g~%"
              (standard-deviation vals))))
:DESCRIBE
    回到煙道損失的那個例子,咱們可使用如下表達式,爲數據集裏的每個觀測量構造一個包含觀測對象的一個列表:
> (setf stack-list
        (mapcar
         #'(lambda (air temp conc loss label)
             (send stack-obs-proto :new
                   :air air
                   :temp temp
                   :conc conc
                   :loss loss
                   :label (format nil "~d" label)))
         air temp conc loss (iseq 0 20)))
(#<Object: 14251a4, prototype = STACK-OBS-PROTO> 
#<Object: 1424164, prototype = STACK-OBS-PROTO> 
#<Object: 1423964, prototype = STACK-OBS-PROTO> 
#<Object: 14231d4, prototype = STACK-OBS-PROTO> 
#<Object: 1422bf4, prototype = STACK-OBS-PROTO> 
#<Object: 14225a4, prototype = STACK-OBS-PROTO> 
#<Object: 1421f74, prototype = STACK-OBS-PROTO> 
#<Object: 14217c4, prototype = STACK-OBS-PROTO> 
#<Object: 1420d74, prototype = STACK-OBS-PROTO> 
#<Object: 1420774, prototype = STACK-OBS-PROTO> 
#<Object: 1420144, prototype = STACK-OBS-PROTO> 
#<Object: 141fad4, prototype = STACK-OBS-PROTO> 
#<Object: 141f484, prototype = STACK-OBS-PROTO> 
#<Object: 141ee44, prototype = STACK-OBS-PROTO> 
#<Object: 141e7e4, prototype = STACK-OBS-PROTO> 
#<Object: 141e114, prototype = STACK-OBS-PROTO> 
#<Object: 142d500, prototype = STACK-OBS-PROTO> 
#<Object: 142cf40, prototype = STACK-OBS-PROTO> 
#<Object: 142c890, prototype = STACK-OBS-PROTO> 
#<Object: 142c170, prototype = STACK-OBS-PROTO> 
#<Object: 142bb30, prototype = STACK-OBS-PROTO>)
數據集對象能夠構形成這樣:
> (setf stack-loss (send data-set-proto :new :data stack-list))
#<Object: 142a100, prototype = DATA-SET-PROTO>
咱們如今能夠檢測一下:loss變量:
> (send stack-loss :describe :loss)
Variable Name: LOSS
Mean: 17.523809523809526
Median: 15.
Standard Deviation: 10.171622523565489
NIL
或者檢測一個派生的:log-loss變量:
> (send stack-loss :describe :log-loss)
Variable Name: LOG-LOSS
Mean: 2.725909553155994
Median: 2.70805020110221
Standard Deviation: 0.5235304969004861
NIL
或者咱們可使用下邊的表達式構造一個關於:log-loss, :air, 和:temp變量的旋轉圖:
> (send stack-loss :plot :log-loss :air :temp)
#<Object: 144ed2c, prototype = SPIN-PROTO>
圖示以下:

相關文章
相關標籤/搜索