デザインパターンは、ソフトウェア設計において繰り返し発生する課題を解決するための定石です。一般的にはオブジェクト指向言語の文脈で語られることが多いですが、Rのような関数型プログラミングの性質を持つ言語においても、柔軟で保守性の高いコードを書くための強力な武器となります。ここでは、不動点アルゴリズムを例に、Rにおける主要なデザインパターンを解説します。
不動点アルゴリズムの基本実装
不動点アルゴリズムは、$x_{n+1} = f(x_n)$ という形式で値を更新し、収束値(不動点)を求める手法です。正の数の平方根を求める関数 $f(x) = p/x$ を例に、基本的な枠組みをRで実装してみます。
find_fixed_point <- function(step_func, initial_x, check_convergence, ...) {
next_x <- step_func(initial_x, ...)
if (check_convergence(initial_x, next_x)) {
return(next_x)
} else {
Recall(step_func, next_x, check_convergence, ...)
}
}
# 平方根を求めるステップ関数
sqrt_step <- function(x, target) target / x
# 収束判定関数
is_converged <- function(v1, v2, tol = 1e-4) {
abs(v1 - v2) < tol
}
このコードを find_fixed_point(sqrt_step, 2, is_converged, target = 2) として実行すると、無限再帰によるエラーが発生します。これは、$x$ が 1 と 2 の間を振動して収束しないためです。この問題を解決するために、既存のロジックを汚さずに機能を追加するパターンを適用していきます。
ラッパーパターン (Wrapper Pattern)
ラッパーパターンは、既存の関数のインターフェースを変えずに新しい機能を追加する手法です。例えば、振動を抑えるために「平均減衰(Average Damping)」という手法を導入します。これは、前回の値と今回の計算値の平均を次の値とするものです。
apply_damping <- function(f) {
function(x, ...) {
new_val <- f(x, ...)
(x + new_val) / 2
}
}
# 減衰を適用して実行
find_fixed_point(apply_damping(sqrt_step), 2, is_converged, target = 2)
# [1] 1.414214
同様に、デバッグ用のログ出力機能もラッパーとして追加できます。これにより、各ステップの計算値を分離して追跡可能です。
with_logging <- function(f) {
function(x, ...) {
cat("Current value:", x, "\n")
f(x, ...)
}
}
find_fixed_point(with_logging(apply_damping(sqrt_step)), 2, is_converged, target = 2)
インターフェースパターン:カリー化とクロージャ
カリー化 (Currying)
アルゴリズムの本体が単一引数の関数を期待している場合、複数の引数を持つ関数を適合させる必要があります。Rでは ... を使った引数の委譲が一般的ですが、匿名関数を使って特定の引数を固定(カリー化に近い処理)することで、インターフェースを整理できます。
# target = 2 に固定した関数を渡す
find_fixed_point(apply_damping(function(x) sqrt_step(x, 2)), 2, is_converged)
クロージャ (Closures)
関数を返す関数(高階関数)としてのクロージャを利用すると、パラメータの固定をより明示的に記述できます。
make_sqrt_step <- function(target) {
function(x) target / x
}
find_fixed_point(apply_damping(make_sqrt_step(2)), 2, is_converged)
キャッシュパターン (Caching Pattern)
反復計算の中で、変化しない重い処理を毎回実行するのは非効率です。ニュートン・ラフソン法のような行列演算を含むケースでは、定数部分をクロージャのスコープ内にキャッシュすることで、実行速度を劇的に向上させることができます。
# ニュートン法による線形回帰の推定
newton_optimizer <- function(mat_x, vec_y) {
# 変化しない行列演算を事前に実行
xtx_inv <- solve(t(mat_x) %*% mat_x)
xty <- t(mat_x) %*% vec_y
# 実際の更新関数のみを返す
function(beta) {
beta + xtx_inv %*% (xty - (t(mat_x) %*% mat_x) %*% beta)
}
}
# データの準備
X_data <- cbind(1, 1:10)
y_data <- X_data %*% c(2, 5) + rnorm(10)
step_nr <- newton_optimizer(X_data, y_data)
find_fixed_point(step_nr, c(0, 0), is_converged)
カウンターパターン (Counter Pattern)
不動点反復が収束しない場合に備え、最大反復回数を制限する必要があります。Rの <<- 演算子を用いたクロージャは、内部状態を持つカウンタとして機能します。
limit_iterations <- function(conv_func, max_iter) {
count <- 0
function(...) {
count <<- count + 1
if (count > max_iter) {
warning("Reached maximum iterations")
return(TRUE)
}
conv_func(...)
}
}
# 最大3回で打ち切る設定
find_fixed_point(
with_logging(sqrt_step),
2,
limit_iterations(is_converged, 3),
target = 2
)
このように、基本となる find_fixed_point のコードを一切変更することなく、「減衰」「ログ出力」「反復制限」といった機能を柔軟に組み合わせることが可能です。これが関数型プログラミングにおけるデザインパターンの真骨頂です。