Scheme入門 第12章 局所変数レット
これまで変数はすべて define
で定義してきました。define
で定義した変数は、トップレベル変数と呼びます。いわゆるグローバル変数です(※1)。それに対して、局所変数(ローカル変数)を定義するlet
, let*
, letrec
という構文もあります。let
は次のように書きます。
(let ((変数1 初期値1)
(変数2 初期値2)
...)
ボディ ...)
具体的に書くと、こんな感じです。
(let ((x 0)
(y 1))
(display (+ x y)))
ここで定義した変数 x
と y
は let
の中だけで有効で、let
から抜けると消えてしまいます。let*
, letrec
も同じ書き方で、使い方も効果もほぼ同じです。何が違うかというと、変数が定義されるタイミングが違うのです。
let
は、(不定の順序で)変数の初期化がすべて終わってから定義します。let*
は、変数を順番にひとつずつ初期化&定義します。letrec
は、すべての変数を定義してから(不定の順序で)初期化します。
さっぱりわかりませんね。それぞれ比較して説明しましょう。まず let
です。
(define x 1)
->
(let ((x 2)
(y x))
(display y))1
let
は、すべての初期化が終わってから変数を定義します。すなわち、y
を初期化するときには、局所変数の x
は、まだ見えていません。そーすると、外側のトップレベル変数 x
を見ることになるので、y
は 1
になります。
(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))
let
や let*
では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)。まぁそれを言うなら、let
も lambda
を使って書けますから、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 での Language Pack の選択
- Scheme の単純な式「(+ 3 2)」, 「(- 10 4)」
- Scheme の単純な式(括弧の入れ子)
- Scheme の単純な式 (DrScheme の定義ウインドウ)
- 関数 area-of-disk の定義と,関数適用
- 関数 area-of-disk,変数 PI の定義と,関数適用(1)
- 関数 area-of-disk,変数 PI の定義と,関数適用(2)
0 コメント:
コメントを投稿