孤独にそっくり

開いている窓の前で立ち止まるな

実践Common Lispを読む 第14章〜第17章

Practical Common Lisp

こんにちは。
今回は第14章〜第17章まで読みました。全体的に読み物っぽく、実際に動かしてみる、というのは多くありませんでした。
特にオブジェクト指向再入門ではずらずらと説明が書かれていることが多く、あまり頭に入ってきませんでした。
ダメダメです。

第14章 ファイルとファイルI/O

*ファイルの読み込み

;;openで開ける
(open "/some/file/name.txt")
;;返ってくるオブジェクトは一つ目の引数として使える。
;;ファイルの最初の行を印字
;;ファイルが存在しない場合の挙動はif-does-not-exist
;;:error、:create、nilの3つがある
(let ((in (open "/some/file/name.txt" :if-does-not-exist nil)))
  (format t "~a~%" (read-line in))
  (close in))

他にもREAD-CHARやREAD-LINE、READがある。READはLispのソースを読む際に使われ、S式を読み込む。READ-SEQUENCEは文字ストリームにもバイナリストリームにも使える。

*ファイルの出力

;; ファイルが存在する場合、:if-existsによって挙動は変わる。
;; :supersedeはファイルを置き換える :appendは終端に追加 :overwriteは頭から上書き
(open "/some/file/name.txt" :direction :output :if-exists :supersede)

データを書き出す関数はいくつかある
WRITE-CHAR、WRITE-LINE、WRITE-STRING、TERPRI、FRESH-LINEなど
いくつかの関数はデータS式として書き出す。PRINTやPRIN1やPPRINTなどだ。

*ファイルを閉じる

WITH-OPEN-FILEを使えばいい

(with-open-file (stream-var open-argument*)
  body-form*)
;;ファイルから1行読み込むとき
(with-open-file (stream "/some/file/name.txt")
  format t "~a~%" (read-line stream))
;;新しいファイルを作る
(with-open-file (stream "/some/file/name.txt" :direction :output)
  (format stream "Some text."))
*ファイル名

ファイル名の別の表現、パスネームというものがある。
パスネームはホスト、デバイス、ディレクトリ、ネーム、タイプ、バージョンという6つの要素を使ってファイル名を表現する構造を持ったオブジェクト。パスネームは様々なOSに対応するための抽象化?らしい
パスネームの説明は長いがおもしくないのでパスすることにした。

第15章 実践:パスネーム可搬ライブラリ

API

ライブラリで提供する操作は、ディレクトリ中のファイルのリストを取ること、与えられたファイルやディレクトリが存在するか判定することである。
処理系がどうのこうのであまり興味がわかないのでパス。

第16章 オブジェクト指向再入門 : 総称関数

ポリモーフィズム、ひとつの概念上の操作がたくさんの異なる具体的な形態を持てること。
LispではTクラスと呼ばれるクラス階層の頂点になるルートクラスが存在する(真偽値Tとは関係ない)。
Common Lispでは複数のスーパークラスを持てる。これを多重継承という。
Lispのオブジェクトシステムでは総称関数というものを作り、それにメソッドを関数と統合した。

*DEFGENERIC

おもちゃ銀行の取引アプリケーションを例にとってみる

;;引き落とし(withdraw)の総称関数
;;総称関数の中身は何もない
(defgeneric withdraw (account amount)
  (:documentation "amountで指定された額を口座から引き落とす
  現在の残高がamountより少なかったらエラーを通知する"))
*DEFMETHOD

次にwithdrawメソッドを定義する。
メソッドは総称関数と同じ数の必須パラメータおよびオプショナルパラメータが必要。

;;balanceはsetf可能な残高を返す関数とする
(defmethod withdraw ((account bank-account) amount)
  (when (< (balance account) amount)
    (error "Account overdrawn."))
  (decf (balance account) amount))
;;引き出せない場合は他の口座から引き落とす
(defmethod withdraw ((account checking-account) amount)
  (let ((overdraft (- amount (balance account))))
    (when (plusp overdraft)
      (withdraw (overdraft-account account) overdraft)))
  (call-next-method))

CALL-NEXT-METHOD関数は適応可能なメソッドを結合するのに使われる総称関数の機構

*メソッド結合

実効メソッドは3つのステップに沿って作られる。
まず、総称関数が実際に渡された引数に基づいて適用可能なメソッドのリストを作る。
次に、そのパラメータの特定子に応じて適用可能メソッドのリストが並び替えられる。
最後に、並び替えられたリストの順番にメソッドが取られ、実効メソッドを作るためにコードが結合される。


このページになんの話をしているのか全くわからない。ちんぷんかんぷん。

*標準メソッド結合

これまで見てきたのは基本メソッドだが、他に補助メソッドと呼ばれる:before、:after、:aroundがある。文章が多くて何もわからない。

*多重メソッド

総称関数の複数の必須パラメータを特定化したメソッドのことを多重メソッドという。


この章さっぱりわからなかったので、こっちを読むことにした。
http://www.geocities.jp/m_hiroi/clisp/clisp01.html

*メソッドの定義

メソッドの特徴は、同じ名前のメソッドをいくつも定義することができ、引数のデータ型によって、その中から実際に呼び出すメソッドを自動的に選択することです。該当するメソッドが見つからない場合はエラーとなります。CLOS では同一名のメソッドの集まりを「総称関数 (generic function) 」と呼びます。この総称関数がC++や Java などのオブジェクト指向とはちょっと違う CLOS の特徴です。

メソッドってクラス内の定義されてるんじゃないんですね。勘違いしてました。
このページ読んだら総称関数のイメージがわきました。

第17章 オブジェクト指向再入門:クラス

総称関数がオブジェクト指向の動詞なら、クラスは名詞だ。

*DEFCLASS

ユーザ定義クラス(これはCommon Lispの言語仕様にある言葉ではないが、ここではSTANDARD-OBJECTのサブクラスをそう呼ぶことにする)はDEFCLASSマクロを使って作ることができる。クラスにはデータ型として3つの側面がある。名前と、他のクラスとの関係と、クラスのインスタンスが持つスロット(フィールド、メンバ)の名前。

(defclass name (direct-superclass-name*)
  (slot-specifier*))
*スロット指定子

DEFCLASSフォームの大部分を占めるのはスロット指定子のリストだ。各スロットはインスタンスにおいて値を保持できる場所である。SLOT-VALUEでアクセスできる。
次の場合は2つのスロットが定義される。

(defclass bank-account ()
  (customer-name
   balance))
;インスタンス化
;戻り値は新しいオブジェクト
(make-instance 'bank-account) -> #<BANK-ACCOUNT {1005717253}>
;使うときはスロットを束縛しないとエラーが通知される
(defparameter *account* (make-instance 'bank-account)) -> *ACCOUNT*
(setf (slot-value *account* 'customer-name) "John Doe") -> "John Doe"
(setf (slot-value *account* 'balance) 1000) -> 1000
;直接slot-valueできるようになった
(slot-value *account* 'balance) ->1000
*オブジェクトの初期化

スロットの値を初期化するのには3種類の方法がある。
DEFCLASSフォームのスロット指定子にオプション:initargか:initformをつける。
あるいはMAKE-INSTANCEによって呼ばれる総称関数INITIALIZE-INSTANCEに対するメソッドを定義する。

;;MAKE-INSTANCEの呼び出し元が顧客名と預金残高の初期値を渡せるようにし
;;さらに預金残高のデフォルト値をゼロとする
(defclass bank-account ()
  ((customer-name
    :initarg :customer-name) 
;;initargは名前を指定するとキーワードパラメータとして使えるようになり、その際の引数がスロットの初期値になる
   (balance
    :initarg :balance
    :initform 0)))
;;initformはLisp式が指定でき、この式の結果はinitarg引数が渡されなかった場合にスロットの値になる

;;口座を作るときにスロットの値を指定できる
(defparameter *account*
  (make-instance 'bank-account :customer-name "John Doe" :balance 1000))

(slot-value *account* 'customer-name) -> "John Doe"
(slot-value *account* 'balance) -> 1000

このとき、:customer-nameに値を渡さないと、slot-valueしたときにエラーになる。
それを防ぐためには:initformでエラーを通知するようにすればいい。

(defvar *account-numbers* 0)

(defclass bank-account ()
  ((customer-name
    initarg :customer-name
    initform (error "Must supply a customer name."))
   (balance
    :initarg :balance
    :initform 0)
   (account-number
    :initform (incf *account-numbers*))))

基本的にはinitargとinitformを組み合わせればいいが、initformのS式には他のスロットの値が使えない。
そこで、INITIALZE-INSTANCEを使う。
例えば、上のクラスにaccount-typeスロットを加え、預金残高に応じて初期値を:gold、:silver、:bronzeにしたい。
その場合には次のようになる。

(defclass bank-account ()
  ((customer-name
    initarg :customer-name
    initform (error "Must supply a customer name."))
   (balance
    :initarg :balance
    :initform 0)
   (account-number
    :initform (incf *account-numbers*)
    account-type))) ;;オプションなしのスロット
;;そして、INITIALIZE-INSTANCEに:afterメソッドを定義すればいい

(defmethod initialize-instane :after ((account bank-account) &key)
  (let ((balance (slot-value account 'balance)))
    (setf (slot-value account 'account-type)
	  (cond
	    ((>= balance 100000) :gold)
	    ((>= balance 50000) :silver)
	    (t :bronze)))))
;;&keyはこのメソッドのパラメータと総称関数のパラメータとが適合できるようにするためで、すべてのメソッドでは使わなくても&keyを指定する必要がある。
*アクセス関数

オブジェクトのスロットに直接アクセスするのは脆弱なコードになってしまう上に変更も大変なので、アクセサ関数を作る。
その時に、bank-accountのサブクラスを定義することがわかっているなら、総称関数として定義すればいい

(defgeneric balance (account))

(defmethod balance ((account bank-account))
  (slot-value account 'balance))

更にSETFマクロを拡張してSETF関数を作ることで便利になる。

;;customer-nameに複数の値を代入できるSETF関数
(defun (setf customer-name) (name account)
  (setf (slot-value account 'customer-name) name))
;;総称関数のSETF
(defgeneric (setf customer-name) value account)
(defmethod (setf customer-name) (value (account bank-account))
  (setf (slot-value account 'customer-name) value))

;;読み込み関数
(defgeneric customer-name (account))
(defmethod customer-name ((account bank-account))
  (slot-value account 'customer-name))

せっかくここまで書いたのに、DEFCLASSには3つのスロットオプションがある(先に言ってほしい)。:readerと:writerと:accessorだ。:accessorは前の2つを両方とも生成するもの。加えて説明する:documentationオプションがある。

*WITH-SLOTSとWITH-ACCESSORS

WITH-SLOTSはSLOT-VALUEでアクセスしたかのようなスロットへの直接アクセスを提供し、WITH-ACCESSORSはアクセサメソッドの簡易記法。

;;WITH-SLOTのフォーム
(with-slots (slot*) instace-form
 body-form*)
;;残高が一定額を下回った時にペナルティを課す関数
(defmethod assess-low-balance-penalty ((account bank-account))
  (with-slots (balance) account
    (when (< balance *minimum-balance*)
      (decf balance (* balance .01)))))
;;2つの口座を統合するメソッド
(defmethod merge-accounts ((account1 bank-account) (account2 bank-account))
  (with-accessors ((balance1 balance)) account1
    (with-accessors ((balance2 balance)) account2
      (incf balance1 balance2)
      (setf balance2 0))))

共有スロット:allocationオプションや多重継承の話はパス。

まとめ

正直あまりよくわからなかったのは、オブジェクト指向がわかっていないからなのか、文章を読み込もうという気持ちにならなかったせいなのかわかりませんが、ひとまず終わりです。
空いた時間で少しずつ読んではいるんですが、内容が濃すぎていつになったら読み終わるのやら。
飽きてしまいそうなのでほかの本も同時に読もうかなと思い始めています。
次回はFORMATと例外処理と特殊オペレータです。