はじめに
年末年始でヒマなので、Haskell ライクな局所的な変数束縛や関数に対してのみ有効な変数束縛を R で実現して遊ぼうというのをやってみました。
動機
何かカッコいいじゃないですか、こういうの。
velocity x1 x2 t1 t2 = dx / dt
where
dx = x2 - x1
dt = t2 - t1
変数の順番考えずに定義だけ並べて宣言的に書けるのとか。スコープがきっちり限定されるのとか。
あとlambda.r
パッケージを見て楽しそうだなと思ったのとか、ですね。
実装
https://github.com/ngr-t/r-bindedEval に置いてます。一丁前にパッケージにしてみたのはパッケージの作り方の勉強も兼ねてです。
使い方
Haskell の let 式の真似をした let 関数はこんな感じです。Haskell 連呼するのって Haskell ガチ勢に殴られそうで怖いですね。
適当に束縛したい変数とその定義を並べて、.in
引数に最終的に評価したい表現を渡します。
z <- let(x = 1, y = 2, .in = x + y)
# x と y には let 関数の外からはアクセスできない
velocity_let <- function (x1, x2, t1, t2) let(
dx = x2 - x1,
dt = t2 - t1,
.in = dx / dt)
velocity_let(80, 90, 0, 0.5)
# [1] 20
もしかすると
z <- let(x = 1, y = 2) %.in% (x + y)
みたいな構文の方がそれっぽいかもしれません。そのうちこっちで書いてみるかもです。
where
構文の真似っこは以下です。パイプを使って書いていますがこちらは関数を第一引数にします。関数の定義の際に使ってください。
velocity_where <- (function (x1, x2, t1, t2) {
dx / dt
}) %>%
where(
dx = x2 - x1,
dt = t2 - t1)
velocity_where(80, 90, 0, 0.5)
# [1] 20
関数定義を()
で括るのはちょっとイケてないのですが、そこは R の演算子の結合順序の関係で、そうしないと関数ではなくて{}
のでくくった部分がwhere()
関数の引数になってしまいます。
let()
および where()
関数両方についてですが、束縛される変数の定義にほかの束縛された変数を使えます。順番は適当でも大丈夫です。なので
let(x = y * 2, y = 1, z = x * y, .in = z)
# [1] 2
とかでも問題ありません。
以下のように相互参照してるのはダメです。
let(x = y, y = x, .in = x + y)
# Error
今回の実装ではこの辺りはigraph::topological.sort()
で評価順序を決めているんですが、delayedAssign()
を使えば、こちらで難しいことを考えなくても R のインタープリタが必要に際してよしなに評価してくれます(あらかた書いたところでこの関数の存在を知りました)。
関数を束縛しても良いんですよ。
fire <- (function (x, w, threshold)
heaviside(weighted_sum - threshold)) %>%
where(
heaviside = function (x) ifelse(x > 0, 1, 0),
weighted_sum = sum(x * w))
全体的にありがたみの薄い例しか挙げられませんでしたね。
パターンマッチング
無茶言わないでください。
所感
R はキモいDSLが書きやすいですね、良くも悪くも。環境操作とか構文パースとか評価順序いじったりとか、いろいろ出来ますほんと。
パッケージ作るのは思っていたほどには面倒ではありませんでした。devtools::build()
が静的言語解析?をやってくれるのも助かりますね。