LL脳がscalaの勉強を始めたよ その20


Scalaコップ本の第7章の残りをやりますよー、今回は変数のスコープからですよー

どうでもいいけどスカラコップってカタカナで書くとオペラみたいな雰囲気ですね、もしくはロボコップの親戚か。

変数のスコープ

変数スコープの基本

Scalaの変数スコープは基本的に中括弧{ }で決まるみたいですね、とりあえずそれっぽいサンプルを写経してみますよー(コップ本が10 * 10なのでちょっと変えました)

サンプルは掛け算の九九表を出力するものデス、それぞれのスコープも一緒に書いていきますよー

def printMultiTable(){
    var i = 1
    // <ココカラ> i がスコープにはいっている
    while(i < 10){
        var j = 1
        // <ココカラ> i と jがスコープに入っている
        while(j < 10){
            val prod = (i * j).toString
            // <ココカラ> i と j と prod がスコープに入っている
            var k = prod.length
            // <ココカラ> i と j と prod と k がスコープに入っている
            while(k < 4){
                print(" ")
                k += 1
            }
            print(prod)
            j += 1
        } // </ココマデ> i と j と prod と k がスコープに入っている
        println()
        i += 1
    }   // </ココマデ>  i と jがスコープに入っている
} // </ココマデ> i がスコープに入っている 

ちょっぴり見にくいかもですが、スコープはこんな感じになりますな。そして、実行結果は次のようになりマス

scala> printMultiTable
   1   2   3   4   5   6   7   8   9
   2   4   6   8  10  12  14  16  18
   3   6   9  12  15  18  21  24  27
   4   8  12  16  20  24  28  32  36
   5  10  15  20  25  30  35  40  45
   6  12  18  24  30  36  42  48  54
   7  14  21  28  35  42  49  56  63
   8  16  24  32  40  48  56  64  72
   9  18  27  36  45  54  63  72  81
スコープ範囲の制約

とりあえずスコープの制約を確認しますよー

まずは変数の使用範囲の制限ですな(当たり前だけども)。スコープの範囲内であれば変数は使えるけども、スコープの範囲外で変数を使おうとするとコンパイラ様に怒られますよー

// 強引に中カッコを書くために;をつけています 
// Scalaのセミコロン推論が上手くサポートできないらしいデス
val a = 1;
// スコープ範囲
{
    val b = 2;
}
println(a)
println(b)

// スコープ範囲エラーですな
(fragment of scope.scala):7: error: not found: value b
println(b)
         ^
one error found

次にスコープ内の変数再定義の禁止デス。同一スコープ内では変数の再定義はできませんよー

{
    val a = 1
    val a = 2
}
// 怒られますなー、ちなみにvarでもダメですよー
(fragment of scope.scala):12: error: a is already defined as value a
  val a = 2;
       ^
one error found
!!!
discarding <script preamble>
シャドウィング

同一スコープ内の再定義はだめなんだけども、スコープの入れ子をひとつ下がれば同一名の別変数として定義して利用できますよー、これをシャドウィングというみたいです

val a = 1;
{
    // 新しいスコープ範囲なので、先に定義したaとは内部的に異なる変数aを定義しますよ
    val a = 2;
    // 二番目に定義されたaのスコープ範囲なので2が出力されます
    println(a)
}
//  一番目に定義されたaのスコープ範囲なので1が出力されます
println(a)

シャドウィングされた場合、スコープ内からスコープの外側の変数にはアクセスできなくなる(上の例では{}の中で一番目のaにアクセスすることができない)ので注意が必要ですな。

ちなみにインタープリタ(対話型のコンソール)で変数の再定義ができるのはシャドウィングされまくりだからみたいですな。

// インタープリタでの実行
scala> val a = 1
a: Int = 1

scala> val a = 2
a: Int = 2

// 実際はこんな感じですよー
val a = 1;
{
    val a = 2
}

とりあえずシャドウィングしまくると混乱を生みそうなので、コップ本でも推奨されているようにできるだけ自重したほうがよさそうですねー。でも使い捨て変数を(規約でぎっちり縛った上で)シャドウィングするのは便利かも。

スコープの例外

例えば以前にやったforの大体表現の{}のように「{}はスコープ範囲」の例外に成りえるものもあるみたいですねー

ちなみにforの代替表現はジェネレータやフィルタを{}で括ることで;を省略できるやつでしたな

val filesHere = (new java.io.File(".").listFiles)
// ここから代替表現
for{
    file <- filesHere
    // ()を使うとフィルタに;が必要
    if file.getName.endsWith(".scala")
    line <- fileLines(file)
    if line.trim.matches(pattern)
} println(file + ": " + line.trim)

命令型をリファクタリングすべし

最初のサンプルとしてあげたは掛け算の九九表コードは命令型全開なので、Scala的指向から関数型に書き換えてしまいますよー

書き換える項目は次の3点

  • print, printlnによる副作用的な画面出力
  • whileの使用
  • varの使用

printとprintlnを排除するので関数名もMultiTableに変更しますよ

// 1行文をシーケンスとして返す処理
def makeRowSeq(row:Int) = {
    // ループしてシーケンスを生成
    for(col <- 1 to 9) yield {
        // 九九の計算結果を文字列として取得
        val prod = (row * col).toString
        // 文字列 * 整数の繰り返し表示で空き領域を計算
        val padding = " " * (4 - prod.length)
        // yieldで表示文字列のシーケンスを生成
        padding + prod
    }
}
// 一行分を文字列として返す
def makeRow(row:Int) = {
    // makeRowSeqで生成したシーケンスをmkStringで結合 
    makeRowSeq(row).mkString
}
// 九九表を完成
def multiTable() = {
    // 表を生成
    val tableSeq = {
        // 各行の文字列シーケンスを生成
        for(row <- 1 to 9) yield {
            makeRow(row)
        } 
    }
    // シーケンスを改行文字で結合
    tableSeq.mkString("\n")
}

// 出力するためのprintln
println(multiTable)

こんな感じで出来ましたー、括り出したmakeRowとmakeRoeSeqはヘルパー関数と呼ぶみたいですね

実際の計算結果はこちらー

% scala multi.scala 
   1   2   3   4   5   6   7   8   9
   2   4   6   8  10  12  14  16  18
   3   6   9  12  15  18  21  24  27
   4   8  12  16  20  24  28  32  36
   5  10  15  20  25  30  35  40  45
   6  12  18  24  30  36  42  48  54
   7  14  21  28  35  42  49  56  63
   8  16  24  32  40  48  56  64  72
   9  18  27  36  45  54  63  72  81
ちょっとメモ

mkStringはシーケンスを結合して文字列にするメソッドみたいですね

// 単純結合
scala> List(1,2,3,4,5).mkString
res0: String = 12345
// 結合文字の指定もできるよー
scala> List(1,2,3,4,5).mkString("-")
res1: String = 1-2-3-4-5

それと文字列 * 整数の演算では文字列繰り返しになりますなー

// hoge3回繰り返しー
scala> "hoge" * 3
res2: String = hogehogehoge

// 交換法則はなりたたないので逆はダメです
scala> 3 * "hoge"
<console>:5: error: overloaded method value * with alternatives (Double)Double <and> (Float)Float <and> (Long)Long <and> (Int)Int <and> (Char)Int <and> (Short)Int <and> (Byte)Int cannot be applied to (java.lang.String)
       3 * "hoge"

いじょうぅ

7章が漸く終わりましたので、次は8章の関数とクロージャーに進みますよー

なんだかんだ1ヶ月続きましたが、まだ4分の1も終わってないというこの事実(´・ω・`)…が、頑張ります