前回の記事で、変数の再宣言についてコメントを頂いたので調べてみました。今回取り扱うテーマは変数(など)の再宣言とシャドウイングです。
はじめに:シャドウイングとは?
fn main() {
let x: i64 = 1;
println!("{}", x); // 1
{
let x: i64 = 2;
println!("{}", x); // 2
}
println!("{}", x); // 1
}
シャドウイングとは、ある特定のスコープにおける変数名とその外側のスコープにおける変数名が同名であった時に、その2つを別の変数として扱う挙動です。
上記のrustのコードでは、中括弧で囲われたブロック内のxとその外側のxは別の変数として扱われます。
ここまでは最近の言語だとよくある話です。今回問題にしたいのは以下の場合です。
問題: rustにおける同一スコープ同名変数の再宣言
fn main() {
let y: i64 = 1;
println!("{}", y); // 1
let y: i64 = 2;
println!("{}", y); // 2
}
rustでは、同一スコープにおける同名変数を再宣言できるようです。個人的に今まで触ってきた言語にはない挙動だったのでびっくりして調べてみました。
今回色々な言語で再宣言を調べてみました。このような再宣言をシャドウイングと呼んでいる方とスコープが違う場合のみをシャドウイングと呼ぶ方がいるのですが、今回はスコープが違うのをシャドウイングshadowing、スコープが同じ場合は再宣言redeclarationとして話を進めていきます。(理解に誤りがあればご指摘いただければ幸いです)。
別の言語ではどうなるか? その1:再宣言できない言語
Go
package main
import "fmt"
func main() {
// shadowing
var x int = 1
fmt.Println(x) // 1
{
var x int = 2
fmt.Println(x) // 2
}
fmt.Println(x) // 1
// redeclared
var y int = 1
fmt.Println(y)
var y int = 2
fmt.Println(y) // Error: y redeclared in this block
}
シャドウイングは行われますが、同一スコープでの再宣言はできません。(shadowingの部分には、便宜的に実行できた場合の出力を書きましたたが、このコードはコンパイルエラーで実行できません)
以降も再宣言に絞って様々な言語の結果を見ていきましょう。
Kotlin
fun main(args: Array<String>) {
val x: Int = 1
println(x)
val x: Int = 2
println(x)
}
Conflicting declarationsというエラーがでて動きません。
Swift
var x: int = 1
print(x)
var x: int = 2
print(x)
こちらもエラーで動きません。
別の言語ではどうなるか? その2:再宣言できる言語
いくつかの言語では、shadowingはあるが、再宣言はできない、と言う感じでした。次は、再宣言できる言語です。
main = do
let x = 1 -- 1
print x
let x = 2 -- 2
print x
Haskellでは、再宣言可能です。ちなみにHaskellの変数(と呼ぶのが適切かはわかりませんが)はimmutable(不変)です。
別の言語ではどうなるか その3:Javascriptの場合
Javascriptの場合少々特殊な挙動をします。varで宣言した場合とletで宣言した場合で挙動が違います。
// 関数のスコープ
var x = 1;
console.log(x); // 1
function f() {
var x = 2;
console.log(x)
}
console.log(x); // 1
f(); // 2
console.log(x); // 1
// ブロック
var z = 3;
console.log(z); // 3
{
var z = 4;
console.log(z); // 4
}
console.log(z); // 4
// 同一スコープでの再宣言
var y = 5;
console.log(y); // 5
var y = 6;
console.log(y); // 6
Javascriptのvarにはブロックスコープが存在せず、再宣言が可能です。
// 関数のスコープ
let x = 1;
console.log(x); // 1
function f() {
let x = 2;
console.log(x)
}
console.log(x); // 1
f(); // 2
console.log(x); // 1
// ブロックスコープ
let z = 3;
console.log(z); // 3
{
let z = 4;
console.log(z); // 4
}
console.log(z); // 3
// 再宣言。ここから下を実行するとエラーで動きません。
let y = 5;
console.log(y); //
let y = 6;
console.log(y); // Error
一方letの方はブロックスコープがあり、再宣言は不可のようです。
別の言語ではどうなるか その4:Rubyの場合(追記、と言うか書き換え)
PythonやRubyなどの変数宣言のない動的型付け言語は除外しようかと思いましたが一応書いておきます。コメントをいただいたのでRubyの変数について調べてみたところ、今話している変数とは全く別のものであることがわかりました。(間違っていたらコメント欄からまさかり飛ばしてください)
x = 1
puts x # 1
x = Array.new()
puts x # 何も表示されず改行もない
x = 'Hello, World!'
puts x # Hello, World!
このようにRubyでは、変数の宣言はありません。更にいえば、型を全く気にせず全てのオブジェクト(Rubyでは全てがオブジェクトであると説明されます)を代入できます。
- 宣言が存在するタイプの言語の変数:宣言したタイミングで、変数に結びつくメモリ領域が確保されます。Rustの場合、メモリ領域が誰のもの(どの変数のもの)かは厳格に管理されます。基本的に一度定義した型を再宣言なしに変えることはできません。
- Rubyの場合:変数はメモリ領域を確保しません。メモリ領域を確保しているのは各オブジェクトであり、変数はその参照でしかありません。参照先を変えるだけなので、全く別の型を気軽に代入することができます。
こういうことみたいです。ざっくりしている上に怪しい理解なので、間違っていたらすみません。
メモリを扱う言語を学べば学ぶほどRubyが難解な言語に見えていた理由がやっとわかりました
まとめ
同一スコープの同名変数を再宣言できる言語は意外とあるんですね。前回の記事でコメントをいただいた言語全ては調べられませんでしたが、immutableな変数をもつ関数型言語で利用されているようです。
調べてみたらこんな記事が出てきました。immutableな変数とredeclarationは相性が良いそうです。まだ私は関数型プログラミングをしっかりとさわれていないため、この記事の内容についてはいまいちピンときていません。
ちなみに、'rust redeclaration'でググってみると、'how to avoid redeclaration'とか'no warning ... , feature or bug'とか出てくるので、再宣言可能な事に対して戸惑っている方は多いみたいですね。