高校数学の窓過去問検索

Scheme入門 第12章 局所変数レット


これまで変数はすべて define で定義してきました。define で定義した変数は、トップレベル変数と呼びます。いわゆるグローバル変数です(※1)。それに対して、局所変数(ローカル変数)を定義するlet, let*, letrec という構文もあります。let は次のように書きます。







(let ((変数1 初期値1)
(変数2 初期値2)
...)
ボディ ...)




具体的に書くと、こんな感じです。





(let ((x 0)
(y 1))
(display (+ x y)))





ここで定義した変数 xylet の中だけで有効で、let から抜けると消えてしまいます。let*, letrec も同じ書き方で、使い方も効果もほぼ同じです。何が違うかというと、変数が定義されるタイミングが違うのです。




  • let は、(不定の順序で)変数の初期化がすべて終わってから定義します。

  • let* は、変数を順番にひとつずつ初期化&定義します。

  • letrec は、すべての変数を定義してから(不定の順序で)初期化します。




さっぱりわかりませんね。それぞれ比較して説明しましょう。まず let です。





(define x 1)
(let ((x 2)
(y x))
(display y))
-> 1




let は、すべての初期化が終わってから変数を定義します。すなわち、y を初期化するときには、局所変数の x は、まだ見えていません。そーすると、外側のトップレベル変数 x を見ることになるので、y1 になります。





(define x 1)
(let* ((x 2)
(y x))
(display y))
-> 2




それに対して let* は、順番にひとつずつ初期化して定義します。すなわち、x が初期化・定義されて、それから y が初期化されるので、y を初期化するときには局所変数の x が見えるのです。そのため 2 となります。




letrec は、再帰関数を定義するのに使います。





(letrec
((fact (lambda (x) (if (> x 0) (* (fact (- x 1))) 1))))
(fact 10))





letlet* ではlambda の中から fact が見えませんので、再帰関数が定義出来ないというわけです。letrec ではすべての変数が定義されてから初期化されますが、次のように初期化中に変数をアクセスするとエラーになってしまいますので注意。すべての初期化が終わってからでないとアクセス出来ません。





(letrec ((x 1)
(y x))
(display (+ x y)))





※ 上記はエラーの例です。




let にはもうひとつ、名前付き let と呼ばれる構文があります。これはループを書きやすくする目的の構文です。





(let r ((n 10))
(display "Hello")
(newline)
(if (> n 1)
(r (- n 1))))





"Hello" を10回表示するプログラムを名前付きletで書いた例(※2)。これは実質的に以下と同じです。





(letrec ((r (lambda (n)
(display "Hello")
(newline)
(if (> n 1)
(r (- n 1))))))
(r 10))





このように letrec を使って書くことも出来ますから、名前付きletを使うかどうかは好みの問題です(※3)。まぁそれを言うなら、letlambda を使って書けますから、lambda さえあれば事足りるわけですが。




第12章・完







    ※1:なんていきなり言われても分からない。その解説は下に続いている通りである。が一応。
    意味的にはグローバル変数とは「どこからでも参照可能な変数」と言う意味である。その「どこ」とはプログラムの事である。プログラムAからでもプログラムBからでもプログラムCからでも参照可能であれば「グローバル変数」である。
    が、一般的には、あまりグローバル変数は用いない方が良い。バグの原因になりかねないからだ。何故なら、「確固とした名前」を付けておけば問題は生じないが、例えば軽い気持ちでxなんて書いておいて、他で変数が必要になって忘れてx等と名づけた為にグローバル変数が書き換えられてしまう、なんて事がままある(「名前の衝突」等と言う)。そう言う危険性はなるべく避けた方が良いだろう。
    基本的には、変数は関数内で「局所変数」として設計してみて、最終的に「どうしても必要だったら」そこで初めてグローバル変数を使った方が良い。
    従って、この章を通過した以上、defineを使ったグローバル変数定義は取り合えずは忘れて構わない。

    ※2:Schemeのnewlineと言うのは、改行命令であるが、実際問題、C言語なんかの経験があれば書くのがまだるっこしくてかなわない。
    本質的にはdisplayはどんな文字列でも一気に表示してしまうので、C言語的な流儀で改行文字を含めて一気に出力してしまった方が、シンプルでメンド臭くない。




      (let r ((n 10))
      (display "Hello\n")
      (if (> n 1)
      (r (- n 1))))




    ここで文字列"Hello"の後ろにくっついている"\n"と言うのが改行文字である。これはディスプレイに表示はされないが、newlineと言う関数と同様の効果を発揮し、しかも短く書ける。
    ちなみに、亀田だったら、同じ命題を次のように書く。まずは表示したい文字列"Hello\n"msgとして局所変数letで先に定義してしまって、名前付きletでrを2引数に設定して再帰を行うだろう。そして文字列のリストlsを生成して一気に出力に変換する。
    具体的には以下の通り。




      (let ((msg "Hello\n"))
      (let r ((n 10) (ls ()))
      (if (zero? n)
      (for-each display ls)
      (r (- n 1) (cons msg ls)))))




    上記のコードで得られるリストlsは最終的には実は次の形である。




      ("Hello\n" "Hello\n" "Hello\n" "Hello\n" "Hello\n"
      "Hello\n" "Hello\n" "Hello\n" "Hello\n" "Hello\n")




    これを一気に出力に変換する為、第10章で扱った高階関数mapの仲間であるfor-eachを用い、関数displayを上で得られたリストlsに適用するだけ、だ。




      (for-each display ls)




    Schemeは実はかなり自由度が高いプログラミング言語で、他にも色々な解が考えられるだろう。基本的なルールを満たしていれば(かつそのルールは他の言語に比べても極めて少ない)かなり自由な書き方が出来るので、色々な書き方を試してみれば良い。

    ※3:とは言っても、初学者にはletrecは見づらくゴチャゴチャしていて、かつ、Schemeユーザーのその殆どは単純な繰り返しを記述する場合、名前付きletを愛用する。従って、名前付きletに慣れた方が良い。
    すぐに名前付きletを書けるようになれば、Schemeに「ある程度慣れた」と言う事が出来る。





以上でScheme入門は唐突に終了です。
原著者の構想ではこの続きも存在する筈だったようですが、結局完結を見ぬままサイトは閉じられてしまいました。
とは言っても、数学的な単純な計算を行うプログラムを作る程度であれば、ここまでで得たSchemeの知識で充分乗りきれる、とは思います。
これ以上のトピックを勉強したい、と言う意欲的な方々は独習Scheme三週間を参照して下さい。
重複している部分もありますが、より詳細な解説が掲載されていますし、「似た項目を何度も何度も復習し、次第に深く掘り下げていく」のが学習行為では意外と肝要です。








DrScheme






  1. DrScheme での Language Pack の選択

  2. Scheme の単純な式「(+ 3 2)」, 「(- 10 4)」

  3. Scheme の単純な式(括弧の入れ子)

  4. Scheme の単純な式 (DrScheme の定義ウインドウ)

  5. 関数 area-of-disk の定義と,関数適用

  6. 関数 area-of-disk,変数 PI の定義と,関数適用(1)

  7. 関数 area-of-disk,変数 PI の定義と,関数適用(2)


0 コメント: