#「階層的なKVリストから値を取り出す」コードを、ブログで公開しました。 http://d.hatena.ne.jp/harry-y/20110208/1297129741 私はけっこう便利に使っているのですが「もっとうまいやり方があるよ」という方がいらっしゃれば、ぜひ教えていただきたいと思っています。車輪の再発明でないといいんだけど。 #私の知る限りではそれを一発でやる機能は無いですね。 refを多重化して ~ が使えればいいんですが、kv-listは型的には普通のリストと区別つかないからなあ。
#refの連鎖に割り込む方法があるといいかな?
#(define-class <kvlist-key> ()
((key :init-keyword :key)
(default :init-keyword :default)))
(define (kvlist-key k :optional (default '()))
(make <kvlist-key> :key k :default default))
(define-method ref ((obj <list>) (k <kvlist-key>))
(get-keyword (~ k'key) obj (~ k'default)))
#|
(let1 obj '(:a (:b c :d #(z (:x 4 :y 2))))
(~ obj (kvlist-key :a) (kvlist-key :d) 1 (kvlist-key :y)))
=> 2
|#
#ちょっと回りくどい感じはあるが。
#<list>に対するrefは(ref <list> <integer>)しか無いから両立できなくはない。同じ手法でalistに対するrefも定義できるかも。アクセス時にいちいちオブジェクトを作ることによる性能低下と、今回たまたま<list>だからkey側で多重定義できたけど一般のコンテナには綺麗に拡張できない、というあたりが気にはなる。
#~ によるアクセスに「型のヒント」をオプショナルで加えられるようにする、というのもあるかも。(~ obj :kv-list :key1 :alist :key2) のような。動的に型が決められる場合でも、(~ obj :hash-table 'key) のように明示することで読みやすくもなる。
#欠点は互換性で、(~ obj :hash-table 'key) は従来通り(ref (ref obj :hash-table) 'key) とも解釈できてしまうので、解釈に特例を設けなければならず、綺麗でない。
#すみません、ちょっと理解に時間がかかってしまいました。kvlist-keyをオブジェクトとして作成して、listとkvlist-keyに対するref手続きを定義すると。~というのがrefを多重化したものだというのは、知りませんでした。これが使えるとなると、いろいろ面白いことができそうですね。ところで今「プログラミング Gauche」を片手に「~」について調べているんですが、どのあたりに記述がありますか?Googleで検索しても「~」って見つけにくいんです。勉強不足で申し訳ありません・・・
#プログラミングGaucheが書かれた時代には~はなかったんじゃなかろうか
#~がofficialになったのは最近ですね。0.9.1だっけ? 0.9だっけ?
#そうなんですか。ネットで調べるとすると、どのあたりでしょう?
#ref*の別名?
#ありがとうございます!発見しました。
#万能アクセサですか。こりゃ便利ですね。
#ずっと0.8でやってきたので、キャッチアップが遅れてました・・・
#(~ obj :a :d 1 :y) なんて書けると最高ですね。(define-method ref ((obj <list>) (k <keyword>)) (get-keyword k obj '())) なんてどうなのかな?
#やってみたら動きました。
#(define-method ref ((obj <list>) (k <keyword>))
(get-keyword k obj '()))
(~ '(:a (:b c :d #(z (:x 4 :y 2)))) :a :d 1 :y)
#→2
#こりゃいいや。
#> (ref <list> <keyword>) で特定化するのはちょっと汎用性に欠けるんですよねー。リストがassoc listで、そのキーに<keyword>が来る可能性もあるので。
#なるほど、そうですね。
#汎用性に関してはちょっとアレなんですが、とりあえずこのアイディアでget-kvを書き換えてみました。
#(define (get-kv keylist kvlist . default)
(let* ((result (apply ~ kvlist keylist)))
(cond ((eq? result '())
(cond ((eq? default '())
#f)
(else
(car default))))
(else
result))))
#従前にくらべてかなりシンプルになったと思います。
#refの拡張が前提になるので、ちょっと強引な気もしてきた・・・
#get-kvと対になるput-kv(指定したキーに対応した値を指定した値に置き換えたKVリストを返す)っていうのも作ってあるんですが、これを同じアイディアで書き換えるには、setterの定義も必要になるんですよね・・・
#変更が入ってくると確かにちょっと厄介ですね。kv-listやalistの変更は通常のコンテナの変更とはちょっとセマンティクスが違いますから。
#get-kvってdefaultがなければfoldひとつで済むんですね。
#(define (get-kv keylist kvlist)
(fold get-keyword kvlist keylist))
#default含めるとこんな感じかな
#(define (get-kv keylist kvlist . rest)
(let-optionals* rest ((default #f))
(let ((v (fold (cut get-keyword <> <> '()) kvlist keylist)))
(if (null? v)
default
v))))
#'() == #f なら簡単だったのに
#そのへん統一的に書くならMaybeモナドみたいな枠組みにせざるを得ないと思う。今回たまたまkv-listの単位元が()だったから「()==#fなら…」って思ったわけで。汎用的には単位元が決まらないので、「あらゆる正当な値」と区別可能な別の何かを使って失敗を表現するしかない。
#確かに汎用的に考えると無理があるんですが、Lispだとデータ構造としてリストが扱いやすいので、'() == #f が便利に見えるんですよね。確証バイアスも否定できませんが。
#koguroさん、「get-kvってdefaultがなければfoldひとつで済むんですね。」って、目から鱗です!そうなんだ。ちょっとやってみよう。
#確かにMaybeモナドが使えると、いろいろと便利そうですね。
#実際にコードを書いていると、本質的なところよりも、例外処理の記述の方が長かったりしますし。
#モナドにしちゃうと、呼び出し側も常にモナドとして扱わなくちゃならないんでちと面倒なんですよ。Iconだったっけな、式が「失敗」という特殊値を返すことができて、「失敗」を扱う式は全部「失敗」という値になって、上の方で結果が失敗だったかどうかをチェックできる、という仕組みがあって、それだと面倒が無いんですが。Maybeを言語組み込みにしちゃってるようなものなのでそれはそれで汎用性に欠けるかもなと思わなくもない。
#なるほど。失敗の扱いは、コーディングの工夫でも何とかなります。実際Pettalでは、Ajax処理を行うある程度のレベルの手続きでは、戻り値を(:result "success" :reason "" ... )のような形で返すようにしていて、上位手続きで失敗をハンドリングできるようにしています。処理に失敗していたら(:result "failure" :reason "not_authorized")のようにするわけです。上位手続きでは失敗をハンドリングした後、さらにその上位手続きにこの戻り値を返します。最後にこの戻り値をJSONにしてクライアント側に返して、クライアント側で失敗処理を行います。当初は戻り値をキーバリューリストにするのは冗長かなとも思ったのですが、この方法はかなり便利です。実は手続きに対する引数もキーバリューリストにしていて、引数から特定の情報をget-kvで取り出し、必要であればput-kvで情報を付け加えて、下位手続きに引数を渡しています。ものすごく癖のあるルールかもしれませんが、これもけっこう便利です。
#それだと上位手続きが原則として失敗かどうかをいちいちチェックしないとならないですよね。Maybeモナドはそのチェック部分を隠せるんですが、隠れているのはモナドの中だけなので、モナド外から呼び出す時にMaybeを外してやらないとならないのが面倒。Icon方式はそれがほぼ完全に隠蔽されるということです。
#例えば (if (f (g (h) (i)) (j)) ...) となっていた場合に、Icon方式だと、hやiが失敗を返した場合、gは呼ばれずに(g (h) (i))全体が失敗となり、したがってfも呼ばれずに(f ...)全体が失敗となります。ifは論理値ではなく式が失敗かどうかを判断して分岐します。この方式であれば、下位手続きの失敗は何もしないでも上位に伝わるので、いちいちチェックする必要がなく、捕まえたいところでifを使うだけで済みます。
#