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


Scalaコップ本の9章に突入しますよー、9章のテーマは制御構造を作るぜ!デス

Scalaは組み込みの制御構造がそんなに無いですが、関数値(値としての関数の意)を利用して自分自身で作っていくことができますです、はい

まあ、とりあえず進めていきましょうかねー

重複するコードの削減

関数処理中の共通処理をうまい具合にまとめていって、シンプルに見やすくしよーぜ!という内容をやりますよ

高階関数でコードの削減

高階関数は関数値を引数や戻り値につかうようなモノを指すみたいですねー、関数で関数を使う⇒多段関数みたいな解釈でいいかしら?

とりあえずサンプルやってみましょうかー

コード削減の対象になるサンプル

サンプルとしてCurrent Directory上のファイルを検索&一覧出力するようなAPIを考えますよー

とりあえず検索条件は次の3つでソレゾレ別のメソッドを用意しますね

  • 末尾検索: ファイルの末尾が指定文字列に一致するものを検索
  • 一致検索: ファイルのいずれかの部分が指定文字列と一致するものものを検索
  • 正規表現検索: ファイル名を正規表現で検索

とりあえずズラズラ書いてみますねー

object FileMatcher {
  // とりあえずCurrent Directoryのファイル名一覧を作成
  private def filesHere = ( new java.io.File(".")).listFiles
  // endsWithを利用してファイル名の末尾が指定文字列と一致するファイルを検索
  def fileEnding(query:String) = {
    for(file <-filesHere; if file.getName.endsWith(query)) yield file
  }
  // containsを利用してファイル名のどこかしらが指定文字列と一致するファイルを検索 
  def fileContaining(query:String) = {
    for(file <-filesHere; if file.getName.contains(query)) yield file
  }
  // matchを利用してファイル名を正規表現検索
  def fileRegex(query:String) = {
    for(file <-filesHere; if file.getName.matches(query)) yield file
  }
}

眺めてみると fileEnding、fileContaining、fileRegexの処理がほとんど同じで、ぶっちゃけ「file.getName.◯◯」しか差がないわけですな

なので、極端なハナシこんな感じの抽象コードが使えれば超幸せになれそうです

def fileMatching(query:String, [検索メソッド]) = {
    for(file <-filesHere; if file.getName.[検索メソッド名](query)) yield file
  }

まあ、Scalaでは上のまんま書くのは無理なんですけど…

そんなときに高階関数を使いますよー

上の抽象関数そのままというのは無理だけども、別の形に書き換えてしまえば似た様なことができるらしいので置き換えてみましょうかー

まず各検索関数から呼び出すヘルパー関数(処理を外出しして共通化した関数)を定義しますよー

// 引数として検索文字queryとフィルタ用関数式matcherを渡します
def fileMatching(query:String, matcher:(String, String) => Boolean) = {
  // フィルタ関数matcherでファイル名が検索条件に合うかどうかを判定します
  for(file <-filesHere; if matcher(file.getName, query)) yield file
}

上記のような共通化された検索式の部品ができたところで、呼び出し元となる検索関数を定義しますよ

フイルタ関数は上の例にもあるとおり第2引数にフィルタ用関数式[ 2つのStringを引数(第1引数がファイル名、第2引数が検索文字)としてとって、戻りがBooleanになるもの] を渡せばばいいわけですなー

とりあえずfilesEndingを上記共通関数を利用して単純化してみますよ

def filesEnding(query:String) = {
    filesMatching(query, (fileName:String, query:String) => fileName.endWidth(query)
}

まだちょっと長いので、前の章でやったプレースフォルダーを使ってさらに省略しますよー

// fileMatchingに第2引数として渡すフィルタ関数式の2つのパラメータを
// プレースフォルダで置き換えてしまいます。(呼び出し時の引数利用は一回限りなので)
def filesEnding(query:String)  = filesMatching(query, _.endWidth(_))

うん、かなりすっきりしましたねー

クロージャーでさらに省略

前の章でやったクロージャーを使うとさらに省略可能みたいなので、やってみますよー

具体的には各検索関数に渡す検索文字列queryは何の加工もされずにfileMatchingに渡される&fileMatchingに渡されるフィルタ関数式で用いる値なので、フィルタ関数式に検索文字列queryを格納(queryにフィルタ関数式を適用)した状態でfileMatchingに渡してやります。

うーん、文章で書くとグダグダになるのでこんな感じデス

// fileMatchingに渡すのは検索文字列込みのフィルタメソッドですよ
def filesEnding(query:String) = fileMatching(_.endsWith(query))

自由変数filesMatching内で利用される自由変数queryが、呼び出し元(外側)のfilesEndingで定義(束縛)されているのでコレはクロージャーになりますねー

では完成した省略済関数式を載せてみますよー

上でクロージャーを使ったのでfileMatchingの引数も変わりますねー、ってことで全体を修正したものがこんな感じです。

object FileMatcher {
     private def filesHere = ( new java.io.File(".")).listFiles
     // クロージャーの利用によって引数が変更されましたよー
     def fileMatching(query:String, matcher:String => Boolean) = {
       for(file <-filesHere; if matcher(file.getName)) yield file
     }
     // 末尾検索
     def filesEnding(query:String) = fileMatching(_.endsWith(query))
     // 一致検索
     def filesContaining(query:String) = fileMatching(_.contains(query))
     // 正規表現検索
     def filesRegex(query:String) = fileMatching(_.matches(query))
}

いやー、ものすごくすっきりしましたよー

いじょうー

勉強会準備とかで疲労とかで力尽きてしまったので今回はここまでー、前の方でやったプレースホルダーやクロージャーの復習がてらにジックリやってみました。

次回はループメソッドとかカリー化とかを…が、がんばります