導入
「困難を分割せよ」というのは元々はデカルトの言葉だそうですが、プログラミングをする上でも基礎になる考え方です。 困難を困難たらしめる困難とは何か、どんどん分割していけばいずれ解は自明なものになります。 ある問題を片付けるプログラムを作るとき、そのプログラムに必要な部品をどんどん作っていけばよいのです。
一方で、プログラムの部品 (関数だったりクラスだったり、それらをまとめたライブラリだったり) が必要な機能を満たしていても、それの使い方を使い手がいつもきちんと詳細まで把握しているとは限りません。 うっかりとつまらない間違いをしてしまうこともあるでしょう。
つまり、ある部品によって出来ることを知っておくのと同様にやってはいけないことを把握することもまた重要で、しかしそれは難しいという現実もあるということです。 プログラムの部品を作るにあたっては、
- 使ってはいけない箇所で使えないように
- もし使ってしまったら素早く発覚するように
- 間違った使い方をしたときの影響範囲が小さくなるように
ということに配慮した設計が必要であると考えられます。
自分自身で使う部品については、作っている時点ではそれをどう使うかはあまりにも自明に思えるが故に細かいことを考えなくなりがちですが、翌日の、翌月の、翌年の自分は他人のようなものです。
さて、 Scheme を取り上げた文章は今日でもしばしば R5RS を前提としたものがあり、複数の値をまとめるオブジェクトとしてはリストとベクタ (たまにクロージャに閉じ込めようとするものもありますが) だけでなんとかしようとしている場面を少なからず見ます。 この記事では R7RS においてはレコードやライブラリがそのような__使ってはいけない場合__を制御するにあたって役にたつということを紹介するものです。
R6RS にもレコードやベクタはありますが R7RS とは異なる構文であることに注意してください。 今回は R7RS の仕様を基準に説明します。
アクセス制御のない場合
たとえば、ごく簡単な事例として人物を表現するデータ構造を考えてみます。
更に以下のような条件を付けることにします。
- 人物データは名前と年齢だけから成る
- 名前が変更されることはない
- 年齢は 1 ずつ増える (減ることはない)
扱うデータはふたつですからペアで表現すれば充分でしょう。
(define (make-person name age)
(cons name age))
(define (person-name person)
(car person))
(define (person-age person)
(cdr person))
(define (person-grow! person)
(set-cdr! person (+ (cdr person) 1)))
これで必要とされる機能は足りていますが、これらの手続きを通さずにオブジェクトを書き換えることは出来てしまいますので、条件を安易に壊してしまえます。
(set-cdr! person (- (cdr person) 1))
データを直接書き換えればどうとでも出来てしまうのです。 「このオブジェクトは人物を表している」ということにしたところで、そこに有るのはただのペアです。
レコードを用いる
ではレコードを導入して同様の機能を作ってみます。
(import (scheme base)
(scheme write))
(define-record-type person (make-person name age) person?
(name person-name)
(age person-age person-age-set!))
(define (person-grow! obj)
(person-age-set! obj (+ 1 (person-age obj))))
(define (person-display obj)
(display "#<person name='")
(display (person-name obj))
(display "' age=")
(display (person-age obj))
(display "'>"))
;; 実行例
(let ((man (make-person "John" 16)))
(person-grow! man)
(person-display man))
ここでは define-record-type
は person
という型を定義しています。 この型は name
と age
というふたつの要素から成るということ、また、それらの要素を取り出したり書き換えたりする手続きも同時に定義されます。
定義をよく見ればわかりますが、 age
の横には person-age
というアクセサと person-age-set!
というモディファイア (書換え用の手続き) が書かれていて、 name
の横には person-name
というアクセサしか書かれていません。 person
型の name
を書き換える機能がないのですから「名前を書き換えることが出来ない」という制約はこれで表現できたことになります。
しかし、まだ年齢に関する制約を表現できていません。 アクセサとモディファイアを隠したいですね。 C++ で言うところの private
のようなものがあればよいのですが。
ライブラリ
R7RS にはライブラリという機能があります。 機能群をまとまりごとに入れることが出来て、どの名前を公開するのかを制御することが出来ます。 見せたくないものは公開しなければ外からは見えません。
さっそく上述の例をライブラリにまとめてみます。
(define-library (person)
(export make-person
person?
person-name
person-age
person-grow!
person-display)
(import (scheme base)
(scheme write))
(begin
(define-record-type person (make-person name age) person?
(name person-name)
(age person-age person-age-set!))
(define (person-grow! obj)
(person-age-set! obj (+ 1 (person-age obj))))
(define (person-display obj)
(display "#<person name='")
(display (person-name obj))
(display "' age=")
(display (person-age obj))
(display "'>"))
))
export
節で指定している名前のみが (person)
ライブラリから公開されている名前です。 (person)
ライブラリを import
したプログラム (またはライブラリ) は (person)
ライブラリから公開されている名前を使えるようになります。
(import (scheme base)
(person))
;; 実行例
(let ((man (make-person "John" 16)))
(person-age-set! man 1) ;; ← エラー:そんな手続きはない!
(person-grow! man)
(person-display man))
ここでは、 person
レコードの要素を書き換える手続きを使えるのは (person)
ライブラリの中だけに限定できました。 export
しない名前は C++ でいうところの private
みたいなものです。
まとめ
これだけだとまだ制約を潜り抜けることは不可能ではないのですが、レコードとライブラリによるアクセス制御の概要は示せたと思います。 馬鹿げた失敗を防ぎましょう。