8日目 | Nim100日ドリル
~スコープと名前~

今日の目標

今日は、変数が「どこから見えるのか」を理解する。

Nimでは、変数を作った場所によって、その変数を使える範囲が決まる。この範囲をスコープという。
関数の中で作った変数は基本的に関数の中だけで使うローカル変数になり、関数の外で作った変数はより広い場所から見えるグローバル変数になる。
また、同じ名前の変数を別の場所で使ったとき、Nimがどちらの変数を見ているのかを確認する。

最後に、関数の中で変数を変更したとき、外側の変数まで変わる場合と変わらない場合を、実際にコードを動かして確認する。

今日の内容

8ー1 ~ローカル変数とは何か~

ローカル変数とは、狭い範囲の中だけで使う変数である。
たとえば、proc の中で var score = 10 と書いた場合、その score は基本的にその proc の中だけで使える。
関数の外からその score を直接使おうとしても、Nimはその名前を見つけられない。
これは不便に見えるかもしれないが、実際にはプログラムを安全にするために必要な仕組みである。

もし、どこで作った変数でもプログラム全体から自由に使えてしまうと、変数の値がどこで変わったのか分からなくなる。
小さなコードならまだ追えるが、ファイルが大きくなり、関数が増えると一気に混乱する。
ローカル変数は、「この変数はこの処理の中だけで使う」という範囲をはっきりさせるための道具である。

Nimでは、proc の中で宣言した変数は、その proc の中で使うための変数になる。
たとえば、計算途中の値、表示用の文字列、一時的な判定結果などはローカル変数にするのが自然である。
外側で使う必要がない値まで外に出すべきではない。

次のコードを考えてみる。

                    
proc showLocalScore() =
  var score = 80
  echo "関数の中のscore: " & $score

showLocalScore()
                    
                

このコードでは、proc showLocalScore() = によって showLocalScore という処理を作っている。
その中にある var score = 80 はローカル変数であり、showLocalScore の中だけで使う変数である。
echo "関数の中のscore: " & $score では、文字列と整数をつなげるために $score と書いている。
Nimでは、文字列と整数をそのまま & で連結できないため、整数を文字列に変換する必要がある。
最後の showLocalScore() は、作った処理を実際に呼び出している行である。

初心者がよくするミスは、関数の中で作った変数を、関数の外でも使えると思ってしまうことである。
たとえば、proc showLocalScore() の中で var score = 100 と書いたあと、関数の外で echo score と書くと、Nimは score を見つけられずエラーにする。
これはNimが意地悪をしているのではない。変数の見える範囲を守っているだけである。

Nimではインデントによって処理のまとまりが見えやすくなる。
proc の中身はインデントして書く。
どこからどこまでが関数の中なのかを目で確認する習慣をつけると、スコープのミスを減らせる。

8ー2 ~グローバル変数とは何か~

グローバル変数とは、関数の外側、つまりファイルの上の方などで作る変数である。関数の外で作った変数は、同じファイル内の複数の場所から使える。
たとえば、var total = 0 を関数の外に書くと、後で作る proc add() の中からも total を使える。

グローバル変数が必要になる場面はある。たとえば、プログラム全体で共有したい設定値、処理回数、簡単な状態管理などである。
今回のような練習では、関数の中と外で変数がどう変わるかを確認するために、グローバル変数を使うと分かりやすい。

ただし、グローバル変数は便利な反面、使いすぎると危険である。
どの関数がその値を変更したのか追いにくくなるからである。
関数が少ないうちは問題にならなくても、コードが増えると「なぜこの値になっているのか」が分からなくなる。
実用的なプログラムでは、必要以上にグローバル変数を増やさず、できるだけ関数の引数や戻り値を使って値を受け渡しする方が安全である。

次のコードを考えてみる。

                    
var count = 0

proc addCount() =
  count = count + 1
  echo "関数の中のcount: " & $count

addCount()
addCount()

echo "関数の外のcount: " & $count
                    
                

このコードでは、var count = 0 が関数の外にあるため、グローバル変数として使われる。
addCount の中では count = count + 1 によって、外側にある count の値を1増やしている。
addCount() を2回呼び出しているため、最終的に count は2になる。
最後の echo "関数の外のcount: " & $count では、関数の外から見た count の値を表示している。

Nimでは、関数の外で var count = 0 と宣言し、関数の中で count = count + 1 と書けば、外側の count を変更できる。
ただし、関数の中で var count = 10 と書くと、新しいローカル変数 count を作ることになる。
この場合、外側の count を変更しているわけではない。ここは非常に重要である。

よくあるミスは、「同じ名前だから同じ変数だろう」と思ってしまうことだ。
Nimでは、同じ名前でも、作られた場所が違えば別の変数になる場合がある。
関数の中で var を付けて同じ名前の変数を作ると、外側の変数ではなく、関数内だけの別変数として扱われることがある。
これを理解しないままコードを書くと、外側の値が変わると思ったのに変わっていない、という混乱が起きる。

8ー3 ~同じ名前を別の場所で使ったときの挙動~

プログラムでは、同じ名前を別の場所で使えることがある。
たとえば、関数の外に var message = "外" があり、関数の中にも var message = "中" がある場合である。
このとき、関数の中で echo message と書くと、Nimは関数の中にある近い方の message を使う。
関数の外で echo message と書くと、外側の message を使う。

このように、同じ名前が複数ある場合、基本的には「より内側にある名前」が優先される。これを名前の隠蔽、またはシャドーイングという。
初心者のうちは用語そのものを暗記する必要はないが、「同じ名前でも内側で作った変数が優先される」と覚えておけばよい。

たとえば、次のコードを考えてみる。

                    
var message = "外側のmessage"

proc showMessage() =
  var message = "内側のmessage"
  echo "関数の中: " & message

showMessage()

echo "関数の外: " & message
                    
                

このコードでは、関数の外に var message = "外側のmessage" がある。
そして、関数の中にも var message = "内側のmessage" がある。
関数の中で echo "関数の中: " & message と書くと、Nimは内側にある message を使う。
関数の外で echo "関数の外: " & message と書くと、外側にある message を使う。

なぜこの仕組みが必要かというと、関数ごとに独立した名前を使えるようにするためである。
すべての関数で変数名が完全に重複禁止だと、プログラムは非常に書きにくくなる。
たとえば、どの関数でも一時的な数値に count や i を使いたいことがある。
ローカル変数のスコープがあるからこそ、関数ごとに同じ名前を安心して使える。

ただし、同じ名前を使いすぎると、読む人が混乱する。特に、グローバル変数とローカル変数に同じ名前を使うと、「今どちらを操作しているのか」が分かりにくくなる。
練習としてはよいが、実用コードでは、意味の違うものには別の名前を付ける方が安全である。

課題1

この課題では、関数の中と外で同じ名前の変数を使ったとき、どちらの値が表示されるかを確認する。
関数の外には coinName という変数があり、関数の中にも同じ名前の coinName を作る。
穴埋め部分を埋めて、期待する出力と同じになるようにする。

                    
var coinName = "Bitcoin"

proc showCoinName() =
  var coinName = ____
  echo "関数の中: " & coinName

showCoinName()

echo "関数の外: " & ____
                    
                

期待する出力は次の通り。

                    
関数の中: NimCoin
関数の外: Bitcoin
                    
                

ヒントとして、関数の中にある coinName と、関数の外にある coinName は別の変数として考えるとよい。
1つ目の穴には、関数の中で表示したい文字列を入れる。文字列なので、ダブルクォーテーションで囲む必要がある。
2つ目の穴には、関数の外にある変数名をそのまま書けばよい。

課題2

次のコードは、関数の中で score を変更しているように見えるが、実際には外側の score は変わらない。
原因は、関数の中で var score = 110 と書いて、新しいローカル変数を作っているからである。
そこで、このコードを改造し、関数の中で外側の score を変更するようにする。

                    
var score = 10

proc addBonus() =
  var score = 110
  echo "関数の中: " & $score

addBonus()

echo "関数の外: " & $score
                    
                

期待する出力は次の通り。

                    
関数の中: 110
関数の外: 110
                    
                

この問題では、関数の中で新しい score を作らないことが重要である。
外側の score を変更したいなら、関数の中で var score = ... と書いてはいけない。
すでにある変数に代入する形で、score = score + 100 のように書く。

改造後の考え方は、まず外側で score を10にする。次に addBonus の中で、その外側の score に100を足す。
そして、関数の中と外の両方で同じ値になっていることを確認する。

課題3

この課題では、入力された名前と点数を使って、関数の中で加算後の点数を計算し、結果を表示する。
入力では、最初に名前を受け取り、次に点数を受け取る。処理では、関数の中でローカル変数 bonusfinalScore を作り、点数が80点以上なら20点、それ未満なら5点を加算する。
出力では、元の点数、加算後の点数、処理回数を表示する。処理回数はグローバル変数として管理する。

出発コードは次の通りである。____ の部分を埋めて完成させる。

                    
import std/strutils

var playCount = 0

proc makeResultMessage(playerName: string, score: int): string =
  var bonus = 0

  if score >= 80:
    bonus = ____
  else:
    bonus = ____

  var finalScore = score + bonus

  result = playerName & "さんの基本点は" & $score & "点、加算後は" & $finalScore & "点である。"

echo "名前を入力してください:"
let playerName = readLine(stdin)

echo "点数を入力してください:"
let scoreText = readLine(stdin)
let score = parseInt(scoreText)

playCount = playCount + ____

echo makeResultMessage(playerName, score)
echo "処理回数: " & $playCount
echo "関数の外にあるscore: " & $score
                    
                

例えば次のように入力したとする。

                    
Alice
80
                    
                

期待する出力は次の通り。

                    
名前を入力してください:
点数を入力してください:
Aliceさんの基本点は80点、加算後は100点である。
処理回数: 1
関数の外にあるscore: 80
                    
                

この課題で確認したいのは、関数の中で作った bonusfinalScore は関数の中だけで使うローカル変数であるという点である。
一方、playCount は関数の外で作っているため、プログラム全体の処理回数として使える。
score は入力から作った外側の変数だが、関数の中で finalScore を作っても、外側の score 自体は変わらない。
だから最後に 関数の外にあるscore: 80 と表示される。

よくあるエラーの1つは、parseInt を使っているのに import std/strutils を書き忘れることである。
この場合、Nimは parseInt という名前を知らないため、未宣言識別子に関するエラーを出す。
直し方は、ファイルの先頭に import std/strutils を書くことである。

もう1つのよくあるエラーは、score と文字列をそのまま連結しようとすることである。
たとえば、"点数は" & score のように書くと、文字列と整数を直接つなげようとしてエラーになる。
直し方は、"点数は" & $score のように $ を付けて、整数を文字列に変換することである。

今日のまとめ

今日は、変数には見える範囲があることを学んだ。
関数の中で作った変数はローカル変数であり、基本的にその関数の中だけで使う。
関数の外で作った変数はグローバル変数として、同じファイル内の複数の場所から使える。
ただし、グローバル変数は便利な反面、値がどこで変わったのか分かりにくくなるため、使いすぎには注意が必要である。

また、同じ名前の変数を関数の中と外で使った場合、内側の変数が優先されることを確認した。
関数の中で var score = ... と書くと、新しいローカル変数を作ることになる。
外側の score を変更したい場合は、var を付けずに score = score + 1 のように代入する必要がある。
ここを曖昧にすると、値を変更したつもりなのに外側の値が変わっていない、という典型的なミスにつながる。

次回は、複数の値をまとめる tuple について学ぶ。





7日目へ

9日目へ

トップへ戻る


お問い合わせ