第 14 章 遅延評価val

遅延評価val (lazy value) は、値の初期化を最初にアクセスされるまで遅延させる方法です。これは実行中に必要とならないかもしれない、計算コストが高い値を扱う場合に有用です。最初の例として、従業員のデータベースを考えましょう。各従業員ごとにマネージャーとチームが決まっています。

case class Employee(id: Int, name: String, managerId: Int) { val manager: Employee = Db.get(managerId) val team: List[Employee] = Db.team(id) }

上記の Employee クラスは、直ちにそのフィールドをすべて初期化し、従業員テーブル全体をメモリにロードします。これは明らかに最善ではなく、フィールドを lazy にすることで簡単に改善できます。このようにして、データベースアクセスを、本当に必要になるまで、また、初めて必要になるまで遅らせます。

case class Employee(id: Int, name: String, managerId: Int) { lazy val manager: Employee = Db.get(managerId) lazy val team: List[Employee] = Db.team(id) }

実際に何が起きているか、いつレコードがフェッチされるか表示するモックアップのデータベースを使って、見てみましょう。

object Db { val table = Map(1 -> (1, "Haruki Murakami", 1), 2 -> (2, "Milan Kundera", 1), 3 -> (3, "Jeffrey Eugenides", 1), 4 -> (4, "Mario Vargas Llosa", 1), 5 -> (5, "Julian Barnes", 2)) def team(id: Int) = { for (rec <- table.values.toList; if rec._3 == id) yield recToEmployee(rec) } def get(id: Int) = recToEmployee(table(id)) private def recToEmployee(rec: (Int, String, Int)) = { println("[db] fetching " + rec._1) Employee(rec._1, rec._2, rec._3) } }

一人の従業員を取り出すプログラムを実行すると、確かにデータベースは遅延評価Val を参照するときのみアクセスされることが、出力によって確認できます。

ほかの 遅延評価val の使い方は、いくつかのモジュールからなるアプリケーションの初期化の順番を解決することです。遅延評価Val が導入される前は、同様のことを object 定義を使用することで実現していました。二つ目の例として、いくつかのモジュールからなるコンパイラを考えてみましょう。最初に、シンボルのためのクラスと2つの事前定義された関数を定義している、単純なシンボルテーブルを見てみます。

class Symbols(val compiler: Compiler) { import compiler.types._ val Add = new Symbol("+", FunType(List(IntType, IntType), IntType)) val Sub = new Symbol("-", FunType(List(IntType, IntType), IntType)) class Symbol(name: String, tpe: Type) { override def toString = name + ": " + tpe } }

symbols モジュールは、Compiler インスタンスでパラメータ化されています。Compiler インスタンスは、types モジュールなどのほかのサービスへのアクセスを提供します。この例では、事前定義された関数が2つ (加算と減算) だけあり、それらの定義は types モジュールに依存しています。

class Types(val compiler: Compiler) { import compiler.symtab._ abstract class Type case class FunType(args: List[Type], res: Type) extends Type case class NamedType(sym: Symbol) extends Type case object IntType extends Type }

2つのコンポーネントをつなぐため、コンパイラオブジェクトを作成して、2つのコンポーネントへ引数として渡します。

class Compiler { val symtab = new Symbols(this) val types = new Types(this) }

残念ながら、この実直的なアプローチは実行時に失敗します。symtab モジュールが types モジュールを必要としているからです。一般的に、モジュール間の依存は複雑になりがちで、正しい順番で初期化するのは難しく、循環があるために不可能なことさえあります。簡単な対処は、そのようなフィールドを lazy にして、正しい順番は compiler に任せてしまうことです。

class Compiler { lazy val symtab = new Symbols(this) lazy val types = new Types(this) }

これで、2つのモジュールは最初のアクセスで初期化され、compiler は期待通りに動くでしょう。

構文

lazy 修飾子は、具体的な値定義でのみ指定できます。値定義におけるすべての型付け規則が遅延評価valにも適用されますが、ひとつだけ制限が取り払われています。それは、再帰的なローカル値が許されることです。