マクロのマッチングを実装しよう - 4.寛容な receive

多値を扱う receive は引数の数に厳格で acond2 と相性が良くない。
例えば

(receive (a b) (values 3 4)
         (+ a b))
=> 7

は (a b) と values 返す多値が同じ個数だから OK。


でも期待しているよりも少ない個数の場合はエラーになる。(Gauche で確認)

(receive (a b) (values 3)
         (+ a b))
=> *** ERROR: received fewer values than expected

receive は call-with-values で実装されていることが多く、先ほどの例は以下のように展開される。

(call-with-values (lambda () (values 3 4))
                  (lambda (a b) (+ a b)))

多値の個数が lamba式の期待する引数の個数よリ少ない場合は、リストの長さを補ってあげれば良さそう。


というわけでリストと期待される長さを渡すとリストを拡張してくれる expand-list という手続きを用意する。
インターフェースというかテストコードは以下の通り。

(assert-check-true "expand-list"
                    (equal? (expand-list '(a b c) 5) '(a b c #f #f))
                    (equal? (expand-list '(a b c) 4) '(a b c #f))
                    (equal? (expand-list '(a b c) 3) '(a b c))
                    (equal? (expand-list '(a b c) 2) '(a b c))
                    (equal? (expand-list '(a b c) 1) '(a b c))
                    (equal? (expand-list '(a b c) -1) '(a b c))
                    (equal? (expand-list '(a b c) 5 0) '(a b c 0 0))
)

デフォルトでは #f がリストの末尾に足りない個数分補われる。
ひょっとしたら既にこういう手続きがあるかもなあ。

(define (expand-list lst len . value)
  (let ((d (- len (length lst))))
    (if (positive? d)
       `(,@lst ,@(make-list d (if (pair? value) (car value) #f)))
       lst)))

→うわぁ。 take* というもっと便利な手続きがあったよ。。
expand-list と違って長すぎる/短すぎるどちらにも対応している。

(use util.list)
(take* '(a b c d) 3)        (a b c)
(take* '(a b c d) 6)        (a b c d)
(take* '(a b c d) 6 #t)     (a b c d #f #f)
(take* '(a b c d) 6 #t 'z)  (a b c d z z)

これを利用すれば先ほどの (values 3) の例は

(call-with-values (lambda () (values 3))
  (lambda args (apply
                (lambda (a b) (+ a b))
                (take* args (length '(a b)) #t 0))))

のように書ける。(ここはじっくり考えないと分からないかも)


上記のコードを一般化し、多値の個数が少ない場合に寛容な receive の拡張版 receive-loose は以下のように書ける。

(define-macro receive-loose
  (lambda (formals expression . body)
    (let ((args (gensym)))
    `(call-with-values (lambda () ,expression)
       (lambda ,args
         (apply (lambda ,formals ,@body)
                (if (pair? ',formals)
                    (take* ,args (length ',formals) #t)
                    ,args)))))))

;; test
(receive-loose (a b) (values 3)
               (assert-check-true "receive-loose"
                                  (= a 3)
                                  (not b)))
(receive-loose (a b) (values 3 4)
               (assert-check-true "receive-loose"
                                  (= a 3)
                                  (= b 4)))

(receive-loose (a b) (values)
               (assert-check-true "receive-loose"
                                  (not a)
                                  (not b)))
(receive-loose (a b) (values 1 2 3 4)
               (assert-check-true "receive-loose"
                                  (= a 1)
                                  (= b 2)))

receive-loose 便利!。
ちなみに LingrGauche 部屋で今回の件に関連する shiro さんのコメントを見かけたので貼っておきます。

# shiro

# どこかで書いた気もするけど…

# ひとつの値を期待しているところに多値が返った場合に余分な値を捨てる、という仕様は現実的にとても便利なんですが

# Gaucheがそれを正式にサポートしていないのは、まさにそれが便利すぎるからなのです

# つまり、正式にサポートしたら、みんな使いたいでしょ?

# 普段は多分使わないけど、あると便利な補助情報なんかも2番目以降の値で返しておこう、みたいに考え出すし。

# それってライブラリの設計からして影響を与えるんだよね。

# で、その機能があると、「他のSchemeには容易ni

# 容易にportできないコード」をそれと意識することなく書けちゃうし、そういうコードを後で見分けるのがとても難しい

# (正規表現構文とか、Gauche特有のライブラリ関数とか構文とか、そういうものはソースをサーチするだけで判別できる。多値と単値の混合はグローバルプログラム解析をやらないと発見できない)

# なので、CL風の多値の扱いを正式にサポートしたら、それはもはやSchemeではない、と私の中では思います。

# あーちなみに、「一つの値だけ欲しいのにreceive書くのめんどいなあ」という向きには、values-refっちうのがあります。

<前:マクロのマッチングを実装しよう3 | 次:マクロのマッチングを実装しよう5>