第3部 R言語のメタプログラミング

wass80

2017-03-10

メタプログラミングとは

プログラムでプログラムを扱うこと

例:

  • Cのプリプロセッサ
  • C++のtemplate
  • LispのS式
  • Rubyの動的なメソッド, クラス

DSL (ドメイン固有言語)

  • メタプログラミングの目的として, DSLがある
  • 特定目的のための固有の言語
  • Rubyでよくあるやつ
  • 出来ることを減らしその分簡単に書きたいという思い

Rのメタプログラミング

  • HaddlyのライブラリはDSLの一例
    • dplyrもggplotも独特な書き方を提供している
    • 特に%>%はメタプログラミングの力を借りていることがわかりやすい

参考本

  • R言語徹底解説
  • Hadley神の本

メタプログラミングの仕組み

  • Rの関数の挙動が関わってくる
  • 変数名の解決
  • 関数の構成要素
    • 引数リスト (formals)
    • 関数本体 (body)
    • 関数定義元の環境 (environment)

変数名の解決

変数を評価するとコピーが得られる

x <- 1:3
y <- x # Copy x 
x[2] <- 9
x
## [1] 1 9 3
y
## [1] 1 2 3

関数でも

touch <- function(x) x[1] <- 8 # Copied x
a <- c(10, 11)
touch(a)
a
## [1] 10 11

毎回コピーするとめっちゃ遅いんでは

  • 遅い
  • R 2.xでは実際にコピーしていた
  • R 3.xではCopy on Modifyを採用している
    • すぐにコピーはせず, 変更するまでコピーしない
    • このため少しマシ

環境

  • 関数の外側の環境の変数を見つけるための仕組み
  • Listのようなもの
i <- "before"
return_i <- function() i
return_i()
## [1] "before"
i <- "after"
return_i()
## [1] "after"

関数の環境

  • 関数は自分の環境と親の環境を持っている
global <- 1
print_env<- function(){
  outer  <- 2
  list(here = ls(), # 自分の環境の名前
      parent = ls(envir = parent.frame())) # 親環境の名前
}
print_list(print_env())
## $here: outer; $parent: c("global", "print_env", "print_list")

クロージャ

  • 外側の関数の環境も見つける
  • 自分の環境にない場合, 親の環境で探す
i = "global"
const <- function(){
  i <- "outer" # Found
  function() i
}
f <- const()
f()
## [1] "outer"

外側の環境の変更

  • <<-大域束縛で外側の変数を書き換える
counter <- function(i = 1){
  function() {i <<- i + 1; i} # Change Outer i
}
f <- counter()
c(f(), f(), f())
## [1] 2 3 4

<-の場合

  • ローカルな変数を作ってしまう
counter <- function(i = 1){
  function() {i <- i + 1; i} # Make Local i
}
f <- counter()
c(f(), f(), f())
## [1] 2 2 2

まとめ: 関数の中の変数

  • 関数は自分の環境と親の環境を持つ
  • 自分の環境で名前を探し, なければ親の環境で探す

  • むやみに参照透過性を崩すのはやめよう
  • <<- 大域束縛演算子は挙動を理解して使うべき

関数の構成

f <- function(x, y = 2) sqrt(x + y)
formals(f) # 引数リスト
## $x
## 
## 
## $y
## [1] 2
body(f) # 関数本体
## sqrt(x + y)
environment(f) # 定義元環境
## <environment: R_GlobalEnv>

関数の定義を見る

  • 名前だけ書くと関数定義を見れる
  • .Primitive()はRで以外で定義された関数
f
## function(x, y = 2) sqrt(x + y)
sum
## function (..., na.rm = FALSE)  .Primitive("sum")

全ては関数

演算子はバッククオートで関数に

1 + 3
## [1] 4
`+`(1, 3)
## [1] 4

ifも関数

制御構造も関数に

if(1 == 2) "T" else "F"
## [1] "F"
`if`(1 == 2, "T", "F")
## [1] "F"

構造も関数に

中括弧も関数 (逐次評価, 最後の引数を返す)

{a <- 1; a + a}
## [1] 2
`{`(a <- 2, a + a)
## [1] 4

括弧も関数 (恒等関数)

(1 + 2)
## [1] 3
`(`(1 + 2)
## [1] 3

ifが関数で大丈夫なの?

実はRは引数を遅延評価する

do_nothing <- function(x) message("ok ?") # xを評価していない
do_nothing(message("args")) # 引数は評価されない
## ok ?
eval_x <- function(x) {x; message("ok ?")}
eval_x(message("args"))
## args
## ok ?

このため, ifが関数でも問題ない

遅延評価でないときのif

もし, ifが引数をすべて評価する場合, どちらのmessasgeも起きてしまう

if (1 == 2) message("T") else message("F")
## F

しかし, Rでは引数は必要になるまで評価されない

条件に応じてどちらかが評価される

ほんとに関数?

  • 極端な例として(を上書きする
`(` <- function(x) x + 1
c(3, (3), (1 + 2) * 3) 
## [1]  3  4 12
  • ヤバイので消す
rm("(")

まとめ: 関数の正体

  • 演算子や制御構造など, すべてが関数で処理されている

Rの引数渡し

  • Rは関数を呼ぶとき, 引数をどのように渡しているだろう

  • 参照の値渡し
    • Ruby, Python, Java, JSなど多くの言語
  • 参照渡し, 値のコピー渡しなどを選べる
    • C++, C#, Rustなど参照の概念のある言語
  • Rはどちらでもない

表現式と環境渡し

  • substituteで引数の表現式が見れる
  • 引数ではこれとプロミスは表現式と元環境
s <- function(x) substitute(x)
s(1 + noname)
## 1 + noname
s(sqrt(5 * (1 + 3)))
## sqrt(5 * (1 + 3))

表現式を文字列に変換

  • library(dplyr)などはこれを利用している
s <- function(x) deparse(substitute(x))
s(1 + 5)
## [1] "1 + 5"
s(library(dplyr))
## [1] "library(dplyr)"

quoteeval

  • quote 表現式を返す
  • eval 表現式を評価
x <- 1
quote(1 + 2)
## 1 + 2
quote(x + 1)
## x + 1
eval(quote(x + 1))
## [1] 2

quotesubstituteの違い

  • substituteはローカルな変数を置き換える
  • quoteはそのまま
f <- function(x = 1) {
  y <- 2
  c(quote(x + y + z),
    substitute(x + y + z))
}
f()
## [[1]]
## x + y + z
## 
## [[2]]
## 1 + 2 + z

環境指定したeval

  • 空の環境はnew.env()で作れる
  • 環境はリストと似た操作ができる
e <- new.env()
e$x <- 5
x <- 10 # Globalの環境
eval(quote(x), e)
## [1] 5
eval(quote(x), globalenv())
## [1] 10

実はリストもevalの環境にできる

eval(quote(x), list(x=5))
## [1] 5
acc <- function(x, key) eval(substitute(key), x)
acc(list(z=1), z)
## [1] 1

filterはこの機能を利用している

filter(iris, Sepal.Length > 7.7)
##   Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
## 1          7.9         3.8          6.4           2 virginica

構文木として扱う

library(pryr)
ast(sqrt(x + y))
## \- ()
##   \- `sqrt
##   \- ()
##     \- `+
##     \- `x
##     \- `y

構文木の構成

  • 定数: スカラー("a", 1)
  • 名前: x, sqrt
  • 呼び出し: ()
  • ペアリスト: (a = 1, b = 3)

構文木の例

ast(function(x, y = 1){z <- x + y; z})
## \- ()
##   \- `function
##   \- []
##     \ x =`MISSING
##     \ y = 1
##   \- ()
##     \- `{
##     \- ()
##       \- `<-
##       \- `z
##       \- ()
##         \- `+
##         \- `x
##         \- `y
##     \- `z
##   \- <srcref>

呼び出し表現式の変更

a <- quote(x + y)
a[[1]] <- `*` # 関数名
a[[2]] <- 5 # 第1引数
a[[3]] <- quote(2 + 3) # 第2引数
a
## .Primitive("*")(5, 2 + 3)
eval(a)
## [1] 25

呼び出し表現式の作成

call("filter", quote(hoge), quote(key <= 5))
## filter(hoge, key <= 5)
  • これで任意の表現式を作ることも,
  • 探索することも出来ることがわかった

大まとめ: メタプログラミング

  • Rは遅延評価
  • 関数には構文木と環境が渡る
  • 構文木をいじることが出来る
  • なんでも出来る

おまけ

こういう関数を準備

lisping <- function(l) {
  if(l[[1]] != as.name("{")) return(l)
  args <- lapply(as.list(l)[c(-1,-2)], lisping)
  f <- as.character(l[[2]])
  do.call(call, c(f, args))
}
Lispy <- function(l) eval(lisping(subs(l)), envir = parent.frame())

おまけ

Lispy(
  {"+"
    {mean
      {c
        1
        10
        {":"
          2
          9}}}
    100}
)
## [1] 105.5

これってS式では??

結論

RはLisp方言だった