こんにちは、VOYAGE GROUPの水越(@Akiyah)です。

最近、仕事でデータ解析環境「R」を使ってデータベースからデータを取り出して加工することが多いです。
そういったとき、データベースからデータをとる部分はSQLでまるまる取得して、その後Rでもりもり加工するのが好きです。
ですがその場合、何度も実行するとSQLの処理が重いしデータベースに負担をかけることになるので、ちょっと困っていました。

そこで今回、Rのメタプログラミングで関数呼び出しをキャッシュ化してみることにしました。
キャッシュと言ってもずっと同じ値を返すのではデータベースに新しいデータが入ったときに更新されないで困るので、ある一定期間(例えば24時間)を過ぎたら最新の値をとるように作ります。

キャッシュ無し

まず最初に、"重い処理"という文字列を出力する関数を作ります。
heavy_func1 <- function() {
  print("重い処理")
  result <- rnorm(1) # ランダムな数字
  return(result)
}
このprint("重い処理")が重いSQLであると思ってください。
heavy_func1を何度か実行すると、
> heavy_func1()
[1] "重い処理"
[1] -2.146503
> heavy_func1()
[1] "重い処理"
[1] 0.07116749
> heavy_func1()
[1] "重い処理"
[1] -0.277692
何度も"重い処理"が実行されているのがわかります。

グローバル変数を用いたキャッシュ

それではキャッシュするバージョンを作ってみましょう。Rでは実行環境(ワークスペース)にデータを持たせて、Rの終了時にファイルに保存して次のR起動時に読み込む事ができるので、グローバル変数にキャッシュを入れておけば今回の用途には十分です。単純に書くとこうなります。
heavy_func2.cache <- NULL
heavy_func2 <- function() {
  now <- Sys.time()
  if(!is.null(heavy_func2.cache)) {
    if (10 > now - heavy_func2.cache$updated_at) {
      return(heavy_func2.cache$value)
    }
  }
  print("重い処理")
  result <- rnorm(1) # ランダムな数字
  heavy_func2.cache <<- list(value=result, updated_at=now)
  return(result)
}
<<-は永続代入と言って、関数定義からグローバル変数に代入するときに使います。キャッシュ(heavy_func2.cache)にはキャッシュする値(value)と更新日時(updated_at)のリストを入れておきます。

実行してみると、
> heavy_func2()
[1] "重い処理"
[1] -0.06969717
> heavy_func2()
[1] -0.06969717
> heavy_func2()
[1] -0.06969717
"重い処理"がはじめの一回しか呼ばれていない事がわかります。そして戻り値はキャッシュが効いて同じ値を返しています。キャッシュの更新時間はここでは10秒にしているので10秒経つともう一度処理が実行されて、値も新しいものになります。
> heavy_func2()
[1] "重い処理"
[1] 0.8203284
ちなみにRでは変数名に"."(ピリオド)が使えます。逆に言うとピリオドが使われていたからと言ってそれはJavaやRubyなどの言語のようなオブジェクトの属性へのアクセスと言うわけではありません。

明示的なグローバル変数宣言を削除

実はこのままだと困る事があります。関数定義の直前にキャッシュのグローバル変数を定義しているので、たとえば関数定義を別ファイルにして何度も呼び出すと、その度にキャッシュが消えてしまうのです。そこを改善してみます。
heavy_func3 <- function() {
  now <- Sys.time()
  if(exists("heavy_func3.cache")) {
    if (10 > now - heavy_func3.cache$updated_at) {
      return(heavy_func3.cache$value)
    }
  }
  print("重い処理")
  result <- rnorm(1) # ランダムな数字
  heavy_func3.cache <<- list(value=result, updated_at=now)
  return(result)
}
グローバル変数を直接定義するのではなく、変数が定義されているかどうか確認する関数existを使うようにした事で、明示的なグローバル変数の定義を消す事ができました。やっとメタプログラミングっぽくなってきましたね。もうちょっと進めてみます。

明示的なグローバル変数アクセスを関数経由に

heavy_func4 <- function() {
  now <- Sys.time()
  cache_name <- paste("heavy_func4", "cache", sep=".")
  if(exists(cache_name)) {
    cache <- get(cache_name)
    if (10 > now - cache$updated_at) {
      return(cache$value)
    }
  }
  print("重い処理")
  result <- rnorm(1) # ランダムな数字
  assign(cache_name, list(value=result, updated_at=now), envir=.GlobalEnv)
  return(result)
}
グローバル変数へのアクセスを全て関数(exists, get, assign)経由にしました。assignの引数にある.GlobalEnvはグローバル環境を表します(このあたりをもっと有効に使う例はまた今度)。

キャッシュを別関数化

そろそろ仕上げです。このキャッシュの仕組みを共通化して別の関数に出します。
cache.get <- function(key) {
  now <- Sys.time()
  cache_name <- paste(key, "cache", sep=".")
  if(exists(cache_name)) {
    cache <- get(cache_name)
    if (10 > now - cache$updated_at) {
      return(cache$value)
    }
  }
  return(NULL)
}

cache.set <- function(key, value) {
  now <- Sys.time()
  cache_name <- paste(key, "cache", sep=".")
  assign(cache_name, list(value=value, updated_at=now), envir=.GlobalEnv)
}

heavy_func5 <- function() {
  cache <- cache.get("heavy_func5")
  if(!is.null(cache)) return(cache)
  print("重い処理")
  result <- rnorm(1) # ランダムな数字
  cache.set("heavy_func5", result)
  return(result)
}
キャッシュしたい関数の最初と最後にちょっと差し込めばキャッシュ化が可能になりました! ふー、一段落ですね。

残りタスク

    残りタスク
  • キャッシュの更新時間を設定できるようにする
  • キャッシュを高階関数化して関数を渡すとキャッシュ化した関数を返すようにする
  • グローバル環境を使わないようにする
  • キャッシュ化する関数のソースコードが変更された場合はキャッシュをクリアするようにする
残りタスクは解決できたらまた報告します。