Scheme SRFI-2 の and-let*
は怖い。
ヒューマンエラーに対して脆弱。
ガード節は ((even? n))
のように二重括弧である必要があるところ
(and-let*
(
:
(n (何か計算式))
((even? n))
:
)
...)
を、うっかり、非重括弧の (even? n)
で、
(and-let*
(
:
(n (何か計算式))
(even? n)
:
)
...)
と書いても問題なく通ってしまって、気づけないんだもの。
and-let*
をコード中につかっていると、どうも怖くてしょうがない。
やらかしているんじゃないかと「ソワソワ」「ハラハラ」落ち着かない。
ヒューマンエラーをわざわざ誘発するような邪悪な構文だ。
(VAR EXPR)
節でのショートカットは危険
SRFI-2 の仕様では (何か計算式)
の値として #f
が許容されないというのはヒューマンエラー以外の意味でも問題。#f
をデータとして扱いたい場合だってあるはずだから。
SRFI-2 のこの部分の定義がよろしくない。
eval_claw[ (VARIABLE EXPRESSION), env ] =
eval[ EXPRESSION, env ]
もっとも、この部分こそが、SRFI-2 が考案された最大の理由だとも言えそうな気もするものの、つまり、例えば連想リストの値の取り出しのようなケース
(and-let*
(
(entry (assoc key assoc-list))
)
...)
でコードをシンプルに書けるというのが、and-let*
最大のメリットのような気もするのだけど、そうだとしても、これの場合でさえ、連想リストの値を変数に束縛するような一行を追加すると問題を抱えるようになる。
(and-let*
(
(entry (assoc key assoc-list))
(value (cdr entry))
)
...)
問題というのは value
が #f
の場合。それを見過ごして、let*
のようなつもりでウッカリこういった束縛節を書いてしまうとバグを抱えることになるというわけ。ガードのつもりは全くないのにガードとして機能してしまう。
まとめ
and-let*
のダメな点をまとまるとこうだ。
- ガードをうっかり非重括弧で書いても、構文的には適法なので、ミスを発見できない。
-
#f
を値として扱うことができない。 - ガードのつもりがない単なる束縛のつもりで書いてもガードとして機能してしまう。
こう考えると、and-let*
はヤラカシた仕様のように思える
そしてこれをよく考えると (VAR EXPR)
節でショートカットさせる仕様が潜在的なバグを誘発する諸悪の根源だとわかる。この仕組みを除去すれば、上の3つの問題は解決できる。
案
束縛節が (変数名 式)
の場合は 式
の評価値が何であろうとも and
をショートカットさせずに次の式を評価するということにしつつ、束縛節の第3項に (where ...)
がある節だけショートカット評価させる。let*-and
とでもいうべきか、そういうのはどうだろう?
(let*-and
(
:
(n (何か計算式) (where (even? n)))
:
)
...)
または第1項目で条件節の記号を指定できる「マーカー付きlet-and*
」もいいかもしれない。マーカーはどんな記号でもよくて、
(let*-and satisfies
(
:
(p (何か計算式➊))
(q (何か計算式➋))
(satisfies (even? (+ p q)))
:
)
...)
とか、
(let*-and where
(
:
(p (何か計算式➊))
(q (何か計算式➋))
(where (even? (+ p q)))
:
)
...)
とか。
なんにせよ、抜き身の「((...))
」というような、ヒューマンエラーを誘発するような邪悪な書式を許さない書き方。
連想配列の中身を取り出すコードはこれを使うとこうなる。(cdr entry)
が #f#
だとしても期待どおりに動く。
(let*-and
(
(entry (assoc key assoc-list) (where entry))
(value (cdr entry))
)
...)
これで記述が少し増えるくらい良いし、むしろ、明示的なコーディングスタイルは読み良いと思う。