Guile基礎/レコード(SRFI-9)

変更履歴

概 要

目 次

参考資料

レコード型の定義

モジュールのロード

下記の define-record-type を利用するためには, 以下に示すように (srfi srfi-9) モジュールをロードする必要があります.
(use-modules (srfi srfi-9))

define-record-type

レコード型は以下の define-record-type 形式を使って定義します.
syntax:
(define-record-type $type$-$name$
  ($constructor$ $field$-$name^*$)
  $predicate$
  $field$-$spec^*$)
$type$-$name$ ::= $identifier$
$constructor$ ::= $identifier$
$field$-$name$ ::= $identifier$
$predicate$ ::= $identifier$
$field$-$spec$ ::= ($field$-$name$ $accessor$)
 $|$ ($field$-$name$ $accessor$ $modifier$)
$accessor$ ::= $identifier$
$modifier$ ::= $identifier$
$identifier$ ::= 識別子

上記の各構成要素は次のような意味を持ちます.

$type$-$name$ 新たに定義するレコード型の型名
$constructor$ $type$-$name$型のデータを生成する手続きの名前
$field$-$name$ $type$-$name$型のデータを構成するフィールドの名前
$predicate$ $type$-$name$型の型検査を行う手続きの名前
$field$-$spec$ 各フィールド($field$-$name$)に対して, そのフィールドの値を取得するための手続きの名前($accessor$)と, 値を設定するための手続きの名前($modifier$)からなる.
$aceessor$ フィールドの値を取得するための手続きの名前
$modifier$ フィールドの値を設定するための手続きの名前

define-record-type は,レコードの型,レコードの情報構造(フィールド), 上で述べた各種手続きを一気に定義します.

例えば,以下は人の情報を扱うためのレコード型を定義しています.
;; record-sample.scm
(define-record-type <person>
  (make-person name character webpage)
  person?
  (name      get-person-name)
  (character get-person-character set-person-character!)
  (webpage   get-person-webpage   set-person-webpage!))
ここで:

注意 $field$-$name$ に指定した各フィールドに対して $field$-$spec$ を指定しないと構文エラーが発生します.以下では,x と y という2つのフィールドを持つレコード型に対して,各フィールドの $field$-$spec$ を指定しなかったために構文エラーが発生しています.1番目のものは x と y の両方の $field$-$spec$ を指定しなかった場合を示していて,2番目のものは y の $field$-$spec$ を指定しなかった場合を示しています.3番目のものは両方のフィールドの $field$-$spec$ を指定した場合を示しています.この場合にはエラーは発生しません.
guile> (define-record-type point (make-point x y) point?)
While compiling expression:
Syntax error:
unknown file:6:0: define-record-type: unknown field in constructor spec in subform x of (define-record-type point (make-point x y) point?)
guile> (define-record-type point (make-point x y) point? (x get-x))
While compiling expression:
Syntax error:
unknown file:7:0: define-record-type: unknown field in constructor spec in subform y of (define-record-type point (make-point x y) point? (x get-x))
guile> (define-record-type point (make-point x y) point? (x get-x) (y get-y))

各種手続きの呼び出し

$constructor$ はフィールドの値を実引数として呼び出します.
($constructor$ フィールドの値 ... )
実引数は define-record-type の中で宣言したフィールドの順番に指定します. この呼び出しは,実引数として指定されたフィールドの値を持つレコード型のオブジェクトを返します.例えば,上記の make-person は次のように呼び出します.
(make-person "AlgoKajya" "A hobbyist programmer" "algokajya.github.io") 
ここで, を示しています.この呼び出しは,それぞれの値からなる<person>型オブジェクトを新たに生成して返します.以下に実行例を示します.
guile> (use-modules (srfi srfi-9))
guile> (load "record-sample.scm")
      ...... コンパイルメッセージ ......
guile> (define algo (make-person "AlgoKajya" "A hobbyist programmer" "algokajya.github.io"))
guile> algo
$1 = #<<person> name: "AlgoKajya" character: "A hobbyist programmer" webpage: "algokajya.github.io">

$predicate$ は任意のオブジェクトを実引数として呼び出します.
($predicate$ オブジェクト)
この呼び出しは,実引数が $predicate$ が示すレコード型オブジェクトならば #t を返し,そうでなければ #f を返します.以下では,変数 algo を束縛しているレコード型オブジェクトと整数値の10に対して preson? を実行してみています.
guile> (person? algo)
$2 = #t
guile> (person? 10)
$3 = #f

$accessor$ はレコード型オブジェクトを実引数として呼び出します.
($accessor$ レコード型オブジェクト)
この呼び出しは $accessor$ が示すフィールドの値を返します. 以下の実行例では,変数 algo を束縛しているレコード型のオブジェクトから氏名, 特性,Webページのアドレスを取り出しています.
guile> (get-person-name algo)
$4 = "AlgoKajya"
guile> (get-person-character algo)
$5 = "A hobbyist programmer"
guile> (get-person-webpage algo)
$6 = "algokajya.github.io"

$modifier$ は, レコード型のオブジェクトとフィールドの値を実引数として呼び出します.
($modifier$ レコード型オブジェクト フィールドの値)
この呼び出しは,レコード型オブジェクト内のフィールドの値を変更するとともに, 第2引数のフィールドの値を返します.以下の実行例では, 変数 algo を束縛しているレコード型のオブジェクト内の character フィールドの値を変更しています.
guile> (set-person-character! algo "An unskilled Schemer")
$7 = "An unskilled Schemer"
guile> algo
$8 = #<<person> name: "AlgoKajya" character: "An unskilled Schemer" webpage: "algokajya.github.io">

レコード型の内部定義

SRFI-9 はレコード型の内部定義(lambda 式や let 式などの本体における定義)を禁止しています.以下は SRFI-9 からの引用です.
Record-type definitions may only occur at top-level (there are two possible semantics for `internal' record-type definitions, generative and nongenerative, and no consensus as to which is better).
筆者が誤解していなければ,「generative」なsemanticsとは, レコード型の内部定義を実行時に行うことを意味していて, 同じ型名の内部定義が2つの異なる本体内にあるとき, それらは別個のレコード型として定義されることになります. 一方,「nongenerative」なsemanticsとは, レコード型の内部定義をコンパイル時に行うことを意味していて, 同じ型名の内部定義が2つの異なる本体内にあっても同じレコード型として定義されることになります(この場合,たぶん,両者の形式が異なると構文エラーになるのだろうと想像します).

Guileはレコード型の内部定義が可能です. 「generative」なsematicsを採用しているようです. 以下のプログラムはこれら2点を確認するためのものです.
;; record.scm
(use-modules (srfi srfi-9))

(define (f x y)
  (define-record-type <point>
    (make-point x y)
    point?
    (x get-x)
    (y get-y))
  (let ((point (make-point x y)))
    (format #t "(In f) point:~A\n" point)
    point))

(define (g point)
  (define-record-type <point>
    (make-point x y)
    point?
    (x get-x)
    (y get-y))
  (if (point? point)
      (format #t "(In g) point:~A IS a g's point.\n" point)
      (format #t "(In g) point:~A IS NOT a g's point.\n" point)))
このプログラムでは,2つの手続き f と g の内部で形式的に等しいレコード型を定義していて,手続き f で生成したレコードオブジェクトが g のレコードオブジェクトとして同定されるか否かを試そうとしています.下記の実行結果を見ると,両方のレコード型は異なるものとして処理されているようです.
$ guile -l record-sample.scm 
      ...... コンパイルメッセージや起動メッセージ ...... 
guile> (g (f 1.0 2.0))
(In f) point:#<<point> x: 1.0 y: 2.0>
(In g) point:#<<point> x: 1.0 y: 2.0> IS NOT a g's point.
$1 = #t

Guileによる拡張機能

モジュールのロード

下記の手続きを利用するためには, 以下に示すように (srfi srfi-9 gnu) モジュールをロードする必要があります.
(use-modules (srfi srfi-9 gnu))

レコードオブジェクトの整形出力

syntax:
(set-record-type-printer! $type$-$name$ $proc$)
$type$-$name$ レコード型の型名
$proc$ 2引数の手続き.第1引数はレコードオブジェクトが渡され,第2引数は出力ポートが渡されます.
返り値 $proc$ をそのまま返します.

上記の $proc$ は,第1引数に渡されたレコードオブジェクトの内容を第2引数の出力ポートに出力する手続きを指定します. このとき,set-record-type-printer! は,$type$-$name$型のオブジェクトを出力するときの出力手続きを $proc$ に設定します.この設定のあと,$type$-$name$型のオブジェクトを出力すると $proc$ を使って出力するようになります.

具体例
;; record-sample.scm
(use-modules (srfi srfi-9))
(use-modules (srfi srfi-9 gnu))

(define-record-type <person>
  (make-person name character webpage)
  person?
  (name      get-person-name      set-person-name!)
  (character get-person-character set-person-character!)
  (webpage   get-person-webpage   set-person-webpage!))

(define (set-person-printer)
  (set-record-type-printer! 
   <person>
   (lambda (person port)
     (format port "<person>\n   Name: ~S\n   Character: ~S\n   WebPage: ~S\n"
             (get-person-name person)
             (get-person-character person)
             (get-person-webpage person)))))
以下の $1 は上記の set-person-printer を実行する前の表示結果を示しています. これはシステム標準の形式で表示されています. 一方,$3 は set-person-printer を実行したあとの表示結果を示しています. これは上で指定した lambda 式を使って表示しています. ちなみに,display や wirte を使って <person> 型オブジェクトを表示しても上記の lambda 式が使われます.
guile> (load "record-sample.scm")
guile> (make-person "AlgoKajya" "A hobbyist programmer" "algokajya.github.io")
$1 = #<<person> name: "AlgoKajya" character: "A hobbyist programmer" webpage: "algokajya.github.io">
guile> (set-person-printer)
$2 = #<procedure 7fe2f8bc34b8 at /org/algo/guile-test/datatype/record-sample.scm:15:3 (person port)>
guile> (make-person "AlgoKajya" "A hobbyist programmer" "algokajya.github.io")
$3 = <person>
   Name: "AlgoKajya"
   Character: "A hobbyist programmer"
   WebPage: "algokajya.github.io"

guile> (display $1)
<person>
   Name: "AlgoKajya"
   Character: "A hobbyist programmer"
   WebPage: "algokajya.github.io"
guile> (write $1)
<person>
   Name: "AlgoKajya"
   Character: "A hobbyist programmer"
   WebPage: "algokajya.github.io"

その他

Guile は immutable なレコード型を定義する仕組みも提供しています. その詳細は Guile[6.6.16 SRFI-9 Records] を参照して下さい.
(おしまい)