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

Scalaコップ本の7章を続けていきますよー、今回はfor式からですねー

for式

Scalaのfor式は反復処理のスイスアーミーナイフです( ー`дー´)キリッ、とのことで単純な反復処理だけではなくていろいろな処理ができるみたいですね。

コレクションの反復処理

一般的な反復処理をやりますよ、っていうことでとりあえずサンプル作ります。内容的にはCurrent Directoryにあるファイルを一覧し表示してみますよー

ちなみにコレクションは、配列とかリストとかMapとかSetとかそういう類のものだと思いこみますよー

// current directoryにあるファイル一覧を取得(Java Apiを使用しまっせ)
val filesHere = (new java.io.File(".")).listFiles
// ファイル一覧を反復して出力
for (file <- filesHere) println(file)            

結果は省略ー、実際に試してみてけさい

ちなみに"file <- filesHere"の箇所はgeneratorと呼ぶみたいですねー、反復処理されるたびにfileHere内の値でvalのfileが初期化されるみたいですねー。valっていうのは覚えておかないとはまりそうだな(´・ω・`)

ついでにfileの型は型推論でやっつけられるみたいですね。filesHereがArray[File]なのでfileの型もFileに推論されるとのことです…うーん、これもおぼえておこう。

Rangeを使ってのコレクション処理

前の例ではJavaApiを使ってコレクションを生成したけども、数値でやりたいときもあるのですが下みたいに数字を並べて書くのはとてもめんどくさいのです。

for (i <- List(1,2,3,4)) println("Iteration " + i)

んじゃRange型を使って書いてみますよー

// a to bがRange型
for (i <- 1 to 4) println("Iteration " + i)       

Pythonでも似たようなことをやりますね、それでは実行結果ですよー

Iteration 1
Iteration 2
Iteration 3
Iteration 4

toの代わりにuntilを使うと上限の値を含めないそうですな、未満みたいなもんかしらね

for (i <- 1 until 4) println("Iteration " + i)

実行すると4の値が除外されてますねー

Iteration 1
Iteration 2
Iteration 3
Scala的ではない反復処理

他の言語ではよくやるパターンにダメ出ししますよー、要素の個数ベースで反復処理をするタイプの例ですね。

for(i <- 0 to filesHere.length - 1) println(filesHere(i)) 

これだと添字の参照がズレてしまう可能性があるのでScala的にはおすすめしませんよーとのことです。ようはwikipedia:イテレータ使おうぜ、wikipedia:イテレータってことですね。

フィルタリング

forでできるのは反復処理だけじゃねーぜ、フィルタリングも処理できるんだぜ!ってことでやってみマス。Current Directory内の.scalaファイルを一覧出力してやりますよー

val filesHere = (new java.io.File(".").listFiles)
// .scalaを末尾にもつファイル名を出力しますよー
for (file <- filesHere if file.getName.endsWith(".scala")) println(file)

こんな要素があるとScalaのforはなんとなくPythonのList内包表現ポイ雰囲気ですねー


ちなみに2行目のfor文は次の式と同じ意味っぽいですねー

for (file <- filesHere)
    if (file.getName.endsWith(".scala")) println(file)

でもScalaのfor式は"<-"で型が決まるコレクションを結果値として返す"式"だという定義に基づくとfor式の中でmフィルターの条件を書くほうが原理主義的に合ってるのかも知れないですねー

ちなみにフィルタ式はいくらでもかけますよー

for(
    file <- filesHere
    // fileかどうかを判定する。ついでに複数のifは;で区切るべし
    if file.isFile;
    if file.getName.endsWith(".scala")
) println(file)
入れ子の反復処理

for式の中に<-を複数入れるとループを入れ子にすることができるみたいですねー

サンプルとして簡易grep処理を作ってみますよー

// ファイルの全行をListとして取得するメソッド
def fileLines(file: java.io.File) = {
    scala.io.Source.fromFile(file).getLines.toList
}
// 簡易的にgrepをする処理
def grep(pattern: String) = {
    for(
        // (使いまわしている)filesHereの値を使って反復処理
        file <- filesHere
        // .scala拡張子のファイルのみを取得
        if file.getName.endsWith(".scala");
        // ファイル内の全行を1行ずつ反復処理
        line <- fileLines(file)
        // パターンに当てはまるもののみ取得
        if line.trim.matches(pattern)
      // 最終的にフィルタされた内容を出力
    ) println(file + ": " + line.trim)
}

使い方は次の通りー

// 正規表現で検索条件を入力
scala> grep (".*hoge.*")

// 結果出力しますよ
./class.scala: class hoge {
./class.scala: val h = new hoge()
./test.scala: var hoge = ""
./test.scala: for(arg <- args) hoge += arg
./test.scala: println(hoge)

後半の式は超冗長に書くとこんな感じの処理になるわけね…うん、まとめたほうが読み易い気がするなぁ

def grep(pattern: String) = {
    for(file <- filesHere){
        if(file.getName.endsWith(".scala")){
            for(line <- fileLines(file)){
                if(line.trim.matches(pattern)){
                    println(file + ": " + line.trim)
                }
            }
        }
    }
}

ついでにジェネレータとフィルタは{}で囲んでもヨサゲ、{} 囲みだと";" の一部を省略できるみたい。コーディング規約的に許されるんなら便利かもね-

def grep(pattern: String) = {
    for{
        file <- filesHere
        // 行末の;を省略
        if file.getName.endsWith(".scala")
        line <- fileLines(file)
        if line.trim.matches(pattern)
    } println(file + ": " + line.trim)
}
変数への中間結果の束縛

for式の反復処理の中で繰り返し使用される計算は変数としてまとめることができるみたいで、コレを束縛変数とよぶみたい。これによって計算量を減らしたりできるみたい

例えば前に定義したgrep式の中でline.trimの処理が反復処理1回につき2度でてきているんだけど、これをtrimmedという束縛変数を使って処理を減らしてみましょーか

def grep(pattern: String) = {
    for(
        file <- filesHere
        if file.getName.endsWith(".scala");
        line <- fileLines(file);
        // 束縛変数定義
        trimmed = line.trim
        // 1度目のline.trimを置き換え
        if trimmed.matches(pattern)
    // 2度目のline.trimを置き換え
    ) println(file + ": " + trimmed)
}

ちなみに束縛変数のスコープはfor式の中だけで、扱い的にはvalになりますなー

// 束縛変数に再代入をしてみるテスト
def grep(pattern: String) = {
    for(
        file <- filesHere
        if file.getName.endsWith(".scala");
        line <- fileLines(file);
        trimmed = line.trim
        if trimmed.matches(pattern)
    ) trimmed = "huga"
}
// valなので再代入禁止でした
<console>:16: error: reassignment to val
           ) trimmed = "huga"
新しいコレクションの作成

これまでのfor式では値の操作だけやってきたけども、新しくコレクションを作成することもできますよー、という内容

何はともあれコレクション作成のためのキーワードyieldを使って定義してみますよー

Current Directoryにある.scalaファイルでArray[File]型のコレクションを作成してみマス

for(file <- filesHere if file.getName.endsWith(".scala")) yield file

それでは実行結果デス、.scalaファイルをまとめたコレクションが無事作成されました

res15: Array[java.io.File] = Array(./Hoge.scala, ./huga.scala, ./file.scala, ./class.scala, ./test.scala, ./moge.scala, ./Rational.scala)

動作的にはforで反復処理が実行されるたびに結果値(上の式だとfile)が生成されて、それらがコレクションとして蓄積されていって結果的にfor式の結果値になるという雰囲気っぽいですね。上の式だと反復処理で生成されるfileがFile型なので、for式全体の結果値はArray[File]型になるみたいデス

yieldは現時点では反復処理のときに(コレクションの作成のために)値を返す程度に覚えておけばいいかしら?

ちなみにyieldキーワードの書き方は次のとおりですなー

for <節> yield <本体> 

節には反復処理のジェネレータやフィルタを、本体には反復処理で実際に行われる処理(上の式だとfile)を書きますよー

なので、下のようなfor式の本体が{}でくくられている形式だと、yieldは{の前に書くひつようがあるわけです

// 本体は{}も含むので{の前にyieldを書きますよ
for(file <- filesHere if file.getName.endsWith(".scala")) yield { file }

// これはダメですな
scala> for(file <- filesHere if file.getName.endsWith(".scala")) { yield file }
<console>:1: error: illegal start of statement
       for(file <- filesHere if file.getName.endsWith(".scala")) { yield file }
もういっちょついでにyield

for式は結果を返す式なので、生成されたコレクションを変数に格納してみますよー。ついでににfor式の節では処理が進むに連れて型変換が次々かかっていきますね。

val forLineLength = {
    for {
        // Array[File]型を生成して処理
        file <- filesHere
        // 末尾が".scala"のファイル名を持つArray[File]型に変換
        if file.getName.endsWith(".scala")
        // Iterator[String]型を生成して処理
        line <- fileLines(file)
        trimmed = line.trim
        // 文字列"for"をもつ Iterator[String]型に変換
        if trimmed.matches(".*for.*")
    // 反復処理中で返されるのはIntなので、for式の結果値はArray[Int]型になりますよ
    } yiled trimmed.length
}

いじょー

for式はもりだくさんなんだけどもこれでも端折っているらしいです。23章で完全版をお届けするらしいので…どんだけてんこ盛りなんだろう…

次回はtry catchあたりをやりますので…が、がんばります