表題

一般化された set!

著者

Per Bothner

状態

この SRFI は現在「確定」の状態である。 SRFI の各状態の説明については ここ を参照せよ。 この SRFI に関する議論については メーリングリストのアーカイブ を参照せよ。

概要

この SRFI では、 手続きの呼び出しが「ロケーションの値」として評価される場合、 その値を set! の最初の引数として指定することで、 そのロケーションの値を 設定する ことができるようにすることを提案する。 例:
(set! (car x) (car y))
この式は次の式に等価になる。
(set-car! x (car y))

多くのプログラミング言語には lvalue という概念がある。 これはロケーションとして「評価」される「式」であり、 代入の左辺に現れることができる。 Common Lisp には、これに関連する「一般化変数」(generalized variable) という概念があり、 それは setf や他のいくつかの特殊フォームの中で使用することができる。 しかし、Common Lisp のこの概念は、 特殊な「ロケーション生成」関数をコンパイル時に認識する というアイデアを基盤としているが、 これは「Scheme の精神」に沿うものではない。

この SRFI では set! を拡張し、 この手続きが Common Lisp の setf と同様の機能をもつようにすることを提案する。 ただし、値の更新を行うのは手続きの戻り値であり、変数の名前ではない。

論拠

代入の左辺に「lvalue」を指定するという方法は、広く使われている。 ほとんどの静的に型付けされた言語で使われているし、 多くの動的に型付けされた言語 (APL や Common Lisp を含む) でも使われている。 このことは、lvalue が人々にとって自然なイディオムであることを示唆している。 一つの理由として、代入のための手続き名を覚えなくて済むという利点があるだろう。 別の理由として、どの式に新しい値が設定され、 どの式が引数であるかが、視覚的に明確であることがある。 さらに、値を評価する式とロケーションを評価する式が視覚的に一貫していることが、 人々にとって自然に感じられるのである。

ほとんどの言語では、 lvalue を生成できる演算子は限られている (主として配列のインデクサとフィールド選択子である)。 言語によっては、lvalue はファーストクラスの値である。 たとえば Algol 68 には、参照型の式がある。 しかし、この型は自動的な強制逆参照とともに使用することで便利なものとなるのであるが、 Scheme のような動的に型付けされた言語ではうまく働かない。 ML はさらに進んででいる。 すべての変更可能な変数はファーストクラスの「セル」であり、 セルの内容にアクセスするには明示的な演算子が必要である。 この方法も Scheme とは相性がよくない。 Scheme では、 ほとんどのコンテキストで変数を使用するときはその値を使用し、 ある種の lvalue コンテキスト (代入の左辺) で変数を使用するときはそのロケーションを使用する、 というモデルにこだわりたい。 このモデルにこだわりながら set! の中で汎用的な「lvalue 式」を使用するには、 「lvalue コンテキスト」の中では 「評価」は通常の評価とは異なった方法で行わなければならないことを意味する。 これが、この SRFI で提案する内容である。

ここが意見の分かれるところであるが、 この SRFI では、すべての Scheme 実装がこの機能をサポートすべきだとは言いたくない。 単に、一般化された set! を実装する場合は、 この SRFI と互換性をもたせるべきだと要求するだけである。

仕様

特殊フォーム set! を拡張し、 最初の引数として、変数だけでなく手続きの呼び出しも受け付けるようにする。 その手続きは、典型的には、何らかのデータ構造から要素を抽出する手続きである。 平たく言うと、 set! の最初の引数として手続きが呼ばれると、 その手続きで抽出される要素が第二引数の値で置換されるのである。 たとえば、
(set (vector-ref x i) v)
この式は次の式に等しい。
(vector-set! x i v)

set! の最初の引数として呼び出される手続きは、 それに対応する「セッター」手続きをもたなければならない。 組み込み手続き setter は、 手続きを受け取り、対応するセッター手続きを返すものとする。

そこで、次の式を

(set! (proc arg ...) value)
次のように定義する。
((setter proc) arg ... value)

ノート: この定義は、Scheme の既存のセッター手続きの慣例にしたがっており、 設定する値は引数の最後に指定する。 たとえば、(setter car)set-car! として定義できる。 次のような別の定義もありえるだろう。

((setter proc) value arg ...) ;; 実際の定義ではない
この定義は、可変個の引数をとる手続きのことを考えると、 より使いやすいものではある。 引数リストの最初に必須引数を 1 つ追加する場合、そのまま素直に追加できるが、 残余引数をもつ引数リストの最後に必須引数を 1 つ追加することは面倒だからである。 しかし、設定する値を引数の最後に指定するという既存の慣例にしたがうほうが、 よりよい仕様となるだろう。

標準セッター

以下の標準手続きには、定義済みのセッターがある。
(set! (car x) v) == (set-car! x v)
(set! (cdr x) v) == (set-cdr! x v)
(set! (caar x) v) == (set-car! (car x) v)
(set! (cadr x) v) == (set-car! (cdr x) v)
....
(set! (caXXr x) v) == (set-car! (cXXr x) v)
(set! (cdXXr x) v) == (set-cdr! (cXXr x) v)
(set! (string-ref x i) v) == (string-set! x i v)
(set! (vector-ref x i) v) == (vector-set! x i v)

セッターの設定とプロパティ

セッター手続きは、 プロパティをもつ手続きという概念の特別なケースである。 他のプロパティとしては、手続きの名前や、手続きの使用法のドキュメントがあるだろう。 1 つの例として (この例は SRFI の仕様の一部ではないが)、 Common Lisp の documentation 関数を考えてみる。
(documentation sqrt)
この式は、sqrt の「ドキュメント文字列」を返す (それが定義されていれば)。 このようなプロパティも、 一般化された set! で設定できるべきである。 たとえば次のように設定する。
(set! (documentation sqrt) "平方根を計算する。")

この SRFI では、この汎用的な「手続きプロパティ」機能を提案しないが、 これと互換性をもたせるべきである。 この SRFI では、特別なケースである setter プロパティの仕様を定める。 次のように定義する:

(set! (setter proc) setter)
この式は、 proc のセッター手続きを setter に設定する。 たとえば Scheme の開始時に、
(set! (setter car) set-car!)
という式が実行されていると考えればよい。

効率について

(set! (foo ..) ...) が好ましいイディオムであるなら、 ((setter foo) ...)(set-foo! ...) と同じくらい効率的にしたいものである。 この効率化は、コンパイラが シンボル foo に束縛されている関数を知っており、 さらに、その関数に束縛されているセッターを知っている場合にのみ可能となる。 Scheme は (Common Lisp と違って) コンパイラやインライン化については何も言及していないので、 ここでは多くを述べることができない。 プログラム全体を解析するコンパイラであれば、 既知の手続きに変更不可能な形で束縛された変数を使用して、 安全にインライン化できる。 また、手続きのセッターが設定されている場合も同様の最適化が可能である。 分割コンパイルがサポートされている場合は、 モジュール システム、コンパイラ スイッチ、あるいは、その他の非標準的な宣言による 特別な情報の助けがなくては、単純な呼び出しやセッターの呼び出しを安全に最適化することはできない。 したがって、私の意見としては、 本質的にこの SRFI によって効率的なコンパイルが困難になることはないと考える。 しかし、次節では、変更不可能な形で手続きにセッターを結びつけるための方法を定義する。 これを使うと、一般化された set! をインライン化する問題を、 通常のインライン化の問題に落とし込むことができる。

getter-with-setter

関数 getter-with-setter は、 プロパティ付きの手続きを定義するために使用する。 具体的にいうと、

(getter-with-setter getter setter)
この式を評価すると新しい無名手続きが生成される。 その手続きを適用すると getter が呼び出される。 また、その手続きのセッターは setter になる。 この関数を使用して手続きを生成した後で その手続きのセッターを変更することはエラーである。

たとえば、次のように定義できるだろう。

(define car (getter-with-setter %primitive-car %primitive-set-car!))
(define set-car! %primitive-set-car!)
このように定義することの利点は、 コンパイラが car をインライン化できるのであれば、 (setter car) もインライン化できるという点にある。

実装

ここ に Twobit 用のサンプル実装がある。

以下に getter-with-setter のサンプル実装を示す:

(define (getter-with-setter get set)
  (let ((proc (lambda args (apply get args))))
    (set! (setter proc) set)
    proc))

著作権

Copyright (C) Per Bothner (1999, 2000). All Rights Reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


編集者: Mike Sperber
最終更新日: Mon Jul 24 12:00:06 MST 2000