Gauche:コレクションの探索

Shiro(2017/05/07 08:38:04 UTC): コレクション中から、条件に合う(述語を満たす)要素を探して何かする、というインタフェースについて。

srfi-1はリストに対するこの操作に2種類のAPIを提供していて、後発のsrfiもほぼそれに準じている。Gaucheは find を汎用のコレクションに適用可能にしている。

find PRED COLL : COLLの要素のどれかがPREDを満たした時に、その要素を返す。どれも満たさなければ #f が返る。

: COLLの要素のどれかがPREDを満たした時に、その要素を返す。どれも満たさなければ が返る。 any PRED COLL : COLLの要素のどれかがPREDを満たした時に、そのPREDの戻り値を返す。どれも満たさなければ #f が返る。(コレクションが順序つきの場合、複数のコレクションを取ってそれぞれからの要素をPREDに渡すことも可。srfiはリストについてそうしている。)

どちらも便利でよく使うんだけど、ちと問題点がある。

srfi-1の find の仕様には欠陥がある。COLLが #f を要素に含んでいてそれがPREDを満たした場合に、見つからなかったのと区別できない。 これは、見つからなかった場合に呼び出される継続をfailure thunkとして渡すことで修正できる。srfi-113の set-find / bag-find はそうなっている。

の仕様には欠陥がある。COLLが を要素に含んでいてそれがPREDを満たした場合に、見つからなかったのと区別できない。 any は、Lispの伝統的な member と同じく、「述語として使える名前っぽいけど、真偽値以上の情報を返すので他の用途にも使える」という設計。だから ? がつかない。でも any という名前は(特に every と対になってると)すんごく述語っぽいので、例えば「コレクションにあるオブジェクトのうち、スロット foo が #f でないものが(ひとつでも)あればそのスロットの値が知りたい」というような場面で (any (cut ~ <> 'foo) coll) が使える、というのはあんまり直感的でない感じだ。

(find P COLL) は (any (^x (and (P x) x)) COLL) と書けるので、 any は find の上位互換と言える。名前の直感性も考慮すると、 (find PRED COLL FAILURE) で「PREDを満たす要素があった場合にPREDの戻り値を返す。無ければFAILUREを呼ぶ」とするのが設計としては妥当のように思える。この設計の find を、区別のために find' と仮に呼んでおく。

srfi-125 (intermediate hash tables)は find' を採用した。しかし他のsrfiとの一貫性が無いので、今後どうなるかは未知数である。

find' はまた、複数の順序つきコレクションに拡張しにくいという問題がある。failure引数を先に持ってくるのも妙だし。(srfi-125でこの問題が話題にのぼらなかったのは、hash tableが順序なしコレクションだからだろうけど、一般化を考えれば議論してしかるべきであった)。

何でこんなことを書いてるかというと、Gaucheのdictionaryフレームワークに find的なものがあったらいいかなと思って書き始めた時に、 hash-table-findがanyの動作であることに改めて気づいたから。

ゼロから設計し直すなら、 dict-find に find' のセマンティクスを与えるのが妥当と思うが、 複数の順序つきコレクションに対する any の需要もあることを考えると、 dict-find は互換性重視で find のセマンティクス(PREDを満たすキーと値を2つの値として返す)にした方が良さそうだ。

アイディア。 (dict-find COLL PRED :optional FAILURE SUCCESS) にして、

COLL中のkey, valueペアについて (PRED KEY VALUE) が呼ばれる

が呼ばれる それが真の値rを返したら、 (SUCCESS KEY VALUE R) が呼ばれてその値が戻り値

が呼ばれてその値が戻り値 ひとつもPREDが満たされなかったら (FAILURE) が呼ばれる

が呼ばれる SUCCESS のデフォルトは (^[k v r] (values k v))

のデフォルトは FAILURE のデフォルトは (^[] (values #f #f))

これだと、デフォルトでsrfi-146の mapping-find のセマンティクス(引数順は他のdict*に合わせてあるが)。anyの動作にしたければ SUCCESS で (^[k v r] r) とすればよい。

要検討

failureとsuccessの順序。success failureの方が覚えやすいと思うが、 実際にはfailureの方だけ指定することの方が多そうな気がする。そうでもないかな? キーワード引数にして順序を考えないというのもありか。

(dict-seek DICT PRED SUCCESS FAILURE) というのを足してみた。 SUCCESS の引数は(predの結果 key value)。

で、これがあれば dict-find (srfi-1 findのセマンティクス) も dict-any も 書ける、と意気込んだのだが、 dict-every でひっかかった。

単純に「全てのk,vペアがPREDを満たすかどうか」という述語と考えるなら述語の意味と 返り値をそれぞれ逆にしてやればいい:

(define (dict-every dict pred) (dict-seek dict (complement pred) (^[r k v] #f) (^[] #t)))

ところがsrfi-1のeveryは 「全ての要素がPREDを満たした場合、最後のPREDの値が返り値となる」ので、 それに合わせるならこの定義はまずい。

anyに比べてeveryのこの機能はそれほど実用性があるとは思えないんだけど、srfi-1では anyとの対称性のためにこうしたんだろうなあ。順序つきコレクションならそれでも 最後にPREDに渡される要素は決まってるけど、順序なしコレクションの場合は 何が渡るかわからないからますますこの仕様の意味が薄れる。

とはいえanyがあるならeveryも欲しいしなあ。PREDで直ちに判断するのではなく、一段噛ませるという手はある:

gen :: key, value -> a check :: a -> bool succ :: a, bool, key, value -> b fail :: a -> b dict-seek:: dict, gen, check, succ, fail -> b

これだとfindとanyはgenにPREDを、checkにidentityを渡せば良く、everyは

(define (dict-every dict pred) (dict-seek dict pred not (^[a b k v] #f) identity))

と書ける。ただ、ちと無理やりな抽象化のようにも見える。

srfi-1 everyのセマンティクスが不要で、述語としてのeveryだけ欲しいなら、anyがあればすぐに書けるのでanyだけでいいような気がしてきた。

Last modified : 2017/05/11 23:44:30 UTC