Scala 遅延評価とAOP

val, lazy val, def について

評価に関連してval, lazy val, def について確認しておく。
これらは評価されるタイミングが異なる。

  • val: valで宣言された変数が所属するスコープがロード(初期化)されたとき評価される。
  • lazy val: valとは異なり、ロードされたときは評価されない。その値が参照されたときに初めて評価される。
  • def: 実行するたびに評価される。

valとdefの違い

val a = {
println("testA")
  10
}
def b = {
println("testB")
  20
}
println(a)
println(a)
println(a)
println(b)
println(b)
println(b)

上記のコードを実行すると出力は以下のようになる

testA
10
10
10
testB
20
testB
20
testB
20

ロードされたときに変数aは評価されてprintln("testA")が実行される。その後実行されるprintln(a)はすべて評価された10が出力される。 defで宣言したメソッドbに格納された値は毎回評価し直すこととなる。そのため毎回testBと20出力結果になる。

このことからdefを使用すると毎回評価されるため、複数回呼ぶ場合は気をつける必要がある。 計算結果のみ参照したい場合はvalで宣言して評価結果を固定するなど意識した方がよさそう。

valと lazy valの違い

具体例

object Main {
  val regularVal: Int = {
    println("Initializing regularVal")
    10
  }

  lazy val lazyVal: Int = {
    println("Initializing lazyVal")
    20
  }

  def main(args: Array[String]): Unit = {
    println("In main method")
    println(regularVal)
    println(lazyVal)
  }
}

上記のコードを実行すると、出力は以下のようになる。

Initializing regularVal  // オブジェクトの初期化時にregularValが評価され、regularValには10という値が格納される
In main method  // mainメソッドが実行されるので、println("In main method")が呼び出される
10  // regularValに格納されている10が出力される
Initializing lazyVal // lazy valは参照時に初めて評価されるのでここで出力される
20

遅延評価とは

評価を遅延して行うこと。値が必要になるまで計算しないこと。

Scalaの遅延評価はlazy valを使う方法と名前付きパラメータを使う方法がある。

lazy valによる遅延評価

lazy valで変数を定義した場合、変数に格納した値の評価を処理開始まで遅らせることができる。

まずはlazy valを使わない例

// Statusによって異なるメソッドを実行する
def executeMethod: Unit = {
        Status match {
        case StatusA => methodA
        case StatusB => methodB
                case StatusC if boolMethod => methodC
                case StatusD if !boolMethod => methodD
        }
}

def methodA: Unit = {}
def methodB: Unit = {}
def methodC: Unit = {}
def methodD: Unit = {}
def boolMethod: Boolean = {
// 複雑な計算を行ったあとbool値を返す
}

StatusDの判定処理が行われた時、2回目のboolMethodが呼ばれる。boolMethodは複雑な計算処理を要するのでできれば評価回数を少なくしたい。そこでlazy valを使って評価回数を削減する。

lazy valを使った例

// Statusによって異なるメソッドを実行する
def executeMethod: Unit = {
        lazy val bool = boolMethod
        Status match {
        case StatusA => methodA
        case StatusB => methodB
                case StatusC if bool => methodC
                case StatusD if !bool => methodD
        }
}

def methodA: Unit = {}
def methodB: Unit = {}
def methodC: Unit = {}
def methodD: Unit = {}
def boolMethod: Boolean = {
// 複雑な計算を行ったあとbool値を返す
}

上記のようにlazy valを使用するとboolMethodが呼び出されるまで評価を遅らせて、boolMethodが複数回呼ばれたとしても評価回数は1回で済む。 ここでvalを使用すると評価回数は1回になるが、boolMethodが呼び出されなかったとしても評価はされるためlazy valを使用する方が性能的には良い。

名前付きパラメータによる遅延評価

メソッドに渡された引数の値の評価をその値が参照されるまで遅らせることができる。

名前付きパラメータを使用しない例

def executeMethod(x: T)(heavyMethod: U): Unit = {
        if (boolMethod(x)) {
                heavyMethod
        }
}

def heavyMethod: U = {
        // 複雑な計算が行われる
        ???
}
def boolMethod(x: T): Boolean = {
        // 単純な処理でbool値を返す
}

上記の場合、heavyMethodが引数に渡された時点で計算される。もしboolMethodがfalseを返す場合、本来計算する必要のない複雑な処理を計算してしまうことになる。このようなケースを回避するために名前付きパラメータを使用する。

名前付きパラメータを使用した例

def executeMethod(x: T)(heavyMethod: => U): Unit = {
        if (boolMethod(x)) {
                heavyMethod
        }
}

def heavyMethod: U = {
        // 複雑な計算が行われる
        ???
}
def boolMethod(x: T): Boolean = {
        // 単純な処理でbool値を返す
}

名前付きパラメータを使用するときは引数を以下のように定義する

(heavyMethod: => U)

このようにすることでheavyMethodは実行されるときに初めて評価される。 そのためboolMethodがfalseの場合はheavyMethodの計算は行われない。

遅延評価を使ったAOP

AOPとは

AOPAspect-Oriented Programming、アスペクト指向プログラミング)は、プログラミングのパラダイムの一つで、クロスカッティングコンサーン(複数のモジュールや関数を横断する機能や関心事)をモジュール化することに重点を置いています。これにより、ソフトウェアのモジュール性を向上させることができます。クロスカッティングコンサーンの例としては、ログ出力、認証、トランザクション管理などが挙げられます。これらの機能はアプリケーションの複数の部分にわたって繰り返し適用されるため、これを繰り返し実装するのは非効率的であり、変更やメンテナンスも難しくなります。

ログ出力を例とする。あらゆるメソッドでログ出力を行いたい場合、メソッドごとにログ出力を書いてしまうと、コードの散乱または重複を引き起こすことになり保守性を低下させてしまう。

遅延評価によりAOPを実現したログ出力

def withLogging[T](taskName: String)(task: => T): T = {
    val startTs = System.currentTimeMillis
    logger.debug(s"[$taskName][Start]")
    val result = task
    val endTs = System.currentTimeMillis
    val durationMs = endTs - startTs
    logger.debug(s"[$taskName][END]($durationMs ms)")
    result
  }

withLogging("Main task"){
        // 実行したい処理を記述
}

上記の例では横断的関心事であるログ出力がビジネスロジックと分離され、保守性と可読性を向上させている。(遅延評価することでログ出力とtaskに渡される処理を明示的に分離している)