副作用と評価順序

スマホからは書きにくくて挫折しましたwww
コードを書くとかムリすぎるwww
帰宅して気力があったら書きます。たぶん。

こんなプログラムを見た。

(define (read-data)
  (let ((a (read-line))
        (b (read-line))
        (c (read-line)))
    `(,a ,b ,c)))

って自分が書いたダメプログラムですがw
何がいけないかを解説します。

(read-line) は呼び出した毎に標準入力からデータを拾うので毎回戻り値が変化します。副作用を伴う処理です。
そして、Scheme の言語仕様として、let の評価順は未規定です*1

つまり、このプログラムにテキストファイルを読み込ませたとき、
a には 1行目のデータが入る場合も有り得ますし、
a には 2行目のデータが入る場合も有り得ますし、
a には 3行目のデータが入る場合も有り得るのです。
なんじゃそりゃ!と思うかも知れませんが、
未規定ということはつまりそういうことなのです。
常に一定になるとは限らないのです。

これではプログラムの意図と反しますよね。
つまりバグを引き起こす恐れがある書き方である*2、ということです。

では、どのように書くのが正解でしょうか。
答えは let* を使います。let* は記述の順番で評価されていきます。
普通の使い方としては、

(define (hoge)
  (let* ((a (+ 2 1))
         (b (+ a 4))
         (c (+ a b)))
    `(,a ,b ,c)))

このように、後の評価式で前の評価結果を使いたい場合に使います。
これは let を入れ子で書けば解決するのですが、ソースがとても汚くなるので
let* で同じ事が書けますよってことなのです。

つまり、元のプログラムでは

(define (read-data)
  (let* ((a (read-line))
         (b (read-line))
         (c (read-line)))
    `(,a ,b ,c)))

のように、 let* を使うことによって、
変数 a の束縛が最初に行われ、次に b、最後に c という順序で変数の束縛が行われます。
let* は let と違い、評価順序が明確に決まっているわけです。

これにより、入力データの 1 行目は必ず変数 a に束縛され、
2 行目は変数 b に、3 行目は変数 c に必ず束縛されることが保証される、というわけです。


本日のポイント:
副作用を伴う処理を使用するときは、評価順序に注意しましょう。

*1:Scheme 処理系の都合によって評価順序が変化する場合があります。うっかり忘れていましたがw

*2:バグったり直ったり、最凶最悪の部類のバグになり得ますw