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


Scalaコップ本の27章をやっていきますよー。27章はモジュール化についてで、今回は実行時リンクからやっていきますよー。

とりあえず今回も利用する、前回までに構築したレシピ検索システムはこんな感じです

// 食品を表現する抽象クラスです
abstract class Food(val name:String){
  override def toString = name
}

// レシピを表現するクラスです
class Recipe(
  // 名前の定義です 
  val name:String,
  // 材料はFoodのリストで保持します
  val ingredients:List[Food],
  // 作り方は文字列で表現デス
  val instructions:String
){
  override def toString = name
}

// テスト用の食品を定義
object Apple extends Food("Apple")
object Orange extends Food("Orange")
object Cream extends Food("Cream")
object Sugar extends Food("Sugar")

// テスト用のレシピを定義しますね
object FruitSalad extends Recipe(
  "fruit salad",
  List(Apple, Orange, Cream, Sugar),
  "全てをまぜます"
)

// DBブラウザクラスから抽象メンバーとして使えるようにクラス化します
abstract class Database {
  // DBの中身になるデータは抽象メンバとして
  // 各DB毎に実装します
  def allFoods:List[Food]
  def allRecipes:List[Recipe]
  // 共通化の部品はこちらで定義しますね
  def foodNamed(name:String) = 
    allFoods.find(f => f.name == name)
  // カテゴリーデータも各DBごとに定義しますネ
  case class FoodCategory(name:String, foods:List[Food])
  def allCategories:List[FoodCategory]
}

// DBクラスを継承してDBオブジェクトを定義します
object SimpleDatabase extends Database {
  // DBの中身やカテゴリを定義してやります
  def allFoods = List(Apple, Orange, Cream, Sugar)
  def allRecipes:List[Recipe] = List(FruitSalad)
  private var categories = List(
    FoodCategory("fruits", List(Apple, Orange)),
    FoodCategory("misc", List(Cream, Sugar)))
  def allCategories = categories
}

// DBブラウザを抽象クラスとして実装してやります
abstract class Browser {
  // 抽象メンバーとしてDBを指定しますね
  val database:Database
  // 抽象メンバーであるDatabaseを利用するように変更します
  def recipesUsing(food:Food) = 
    database.allRecipes.filter(recipe => 
      recipe.ingredients.contains(food))
  def displayCategory(category:database.FoodCategory){
    println(category)
  }
}

// ブラウザクラスを継承します
object SimpleBrowser extends Browser {
  // 必要な処理は全てクラスで定義したので
  // 利用するDBのみを指定してやります
  val database = SimpleDatabase
}

// 学生用DBを定義しますデス
object StudentDatabase extends Database {
  // Foodを拡張して冷凍食品オブジェクトを定義します
  object FrozenFood extends Food("FrozenFood")
  // レンジでチンするレシピを定義です
  object HeatItUp extends Recipe(
    "heat it up",
    List(FrozenFood),
    "Microwave the 'food' for 10 minutes.")
  // レシピと食品とカテゴリーを定義子ますね
  def allFoods = List(FrozenFood)
  def allRecipes = List(HeatItUp)
  def allCategories = List(
    FoodCategory("edible", List(FrozenFood)))
}

//  StudentDatabase 向けブラウザを定義します
object StudentBrowser extends Browser {
  val database = StudentDatabase
}

ふぅー、さすがに長いですな(´・ω・`)今回もこいつを使って進めていきますよ

実行時リンク

Scalaのモジュール機能としてコップ本的に一押しなのは、モジュールのリンクは実行時に動的に行われるために処理の途中経過によってリンクするモジュールを切り替えることが出来ることみたいです。

例えば実行時に渡されたパラメータに応じて利用するデータベースモジュールを切り替えるようなサンプルは次のようになりますね(´・ω・`)

object GotApples {
  def main(args:Array[String]){
    // 渡されたパラメータによってリンクするモジュールを切り替えます
    val db:Database =
      // 学生の場合は学生用データベースをリンクします
      if(args(0) == "student")
        StudentDatabase
      // それ以外はSimpleデータベースをリンクします
      else
        SimpleDatabase 
    object browser extends Browser {
      val database = db
    }
    val apple = SimpleDatabase.foodNamed("Apple").get
    for(recipe <- browser.recipesUsing(apple))
      println(recipe)
  }
}

それではコンパイルして実行してみますよ

// 学生用メニューにはりんごを使ったものはありませんな
% scala GotApples student
// 学生以外(Simple)なものにはりんごを使うメニューが有ります
% scala GotApples simple
fruit salad

きちんと動的にリンクが切り替えられてますな(`・ω・´)ただし上記コードはSimpleDatabaseとStudentDatabaseへのハードリンクが含まれているので、完全にモジュール化するにはハードリンク項目を外出しに擦る必要がありますデス

Scalaコードのコンフィギュレーション

どのタイミングでどのモジュールを利用するかというアプリケーションの「コンフィギュレーション」を定義する場合、Spring等ではXMLを利用するのに対してScalaではScalaファイルを利用することができるみたいです。なので実行前のコンパイルでチェックできる ScalaΣ( ゚Д゚) スッ、スゲー!!ぜ…というのはコップ本の弁です(´・ω・`)これはXMLリテラルがあるからってことかな?

モジュールインスタンスの管理

これまで実装してきた各DBブラウザやDBモジュールは別々のモジュールになっているため独自の内容になっています。なので例えば次のようにSimpleBrowserとStudentDatabaseのFoodCategoryとは各々異なるクラスになるので、相互利用は出来ないデス。

// StudentDatabaseのカテゴリ情報です
scala> val category = StudentDatabase.allCategories.head
category: StudentDatabase.FoodCategory = FoodCategory(edible,List(FrozenFood))

// SimpleDatabaseのカテゴリとは違うので相互運用できません
scala> SimpleBrowser.displayCategory(category)          
// 違うクラスだ(#゚Д゚)ゴルァ!!って言われました(´・ω・`)
<console>:18: error: type mismatch;
 found   : StudentDatabase.FoodCategory
 required: SimpleBrowser.database.FoodCategory
       SimpleBrowser.displayCategory(category)
                                     ^

もしも上記のような操作を成功させるために全てのDBモジュールのFoodCategoryを同じものにしたければFoodCategoryの定義をクラスやトレイトとして外出ししてやれば良いみたいです。しかしながら仮にFoodCategoryを外出しにして共通化した場合でもコンパイラが違う型だから扱えないよエラーを出力する場合があるみたいです。

シングルトン型を使えばいいじゃない

上記のようにトレイト等で外出しした定義に対する型チェックのエラーはシングルトン型を使うことで解決できることが多いみたいです。

例えば次のようなエラーですね。

object GotApples {
  def main(args:Array[String]){
    val db:Database =
      if(args(0) == "student")
        StudentDatabase
      else
        SimpleDatabase 
    object browser extends Browser {
      val database = db
    }
    
    // DB内のカテゴリをブラウザオブジェクトで表示しようとします…が
    for(category <- db.allCategories)
      browser.displayCategory(category)
    
    val apple = SimpleDatabase.foodNamed("Apple").get
    for(recipe <- browser.recipesUsing(apple))
      println(recipe)
  }
}

//// コンパイラに突っ込むと
// 違う方じゃねーの?無理ッスって言われます。
<console>:29: error: type mismatch;
 found   : db.FoodCategory
 required: browser.database.FoodCategory
             browser.displayCategory(category)
                                     ^

上記のようなエラーをシングルトン型を使って解決するにはGotAplesオブジェクトのbrowser.databaseの定義を次のように書き換えてやるのがいいみたいです。

object browser extends Browser {
  // シングルトン型を定義します
  // これによってdatabase型はオブジェクトをひとつしか取らないラシイデス
  val database:db.type = db
}

これによってコンパイラがdbとbrowser.databaseが同じ物だと判定するみたいです。ただし特殊すぎてシングルトン型はこういうケース以外では使いどころが難しいみたいですね(´・ω・`)

いじょうー

とりあえず27章はこれで終わりですー。次回からは28章のオブジェクトの等価性について入っていきマス(`・ω・´)。あと6章か…が、頑張ります