LoginSignup
18
20

More than 5 years have passed since last update.

R にラムダ式を導入する #rstatsj

Last updated at Posted at 2014-11-20

※この記事をもとにパッケージを作成しました
R にラムダ式を導入するパッケージ lambdaR を作った

(※恥ずかしながら、無名関数とラムダ式の違いがよく分かっていないのですが、この記事では、ラムダ式は無名関数の生成を簡単にする方法ということにして話を進めます)

いくつかのプログラミング言語には、無名関数を生成する方法として、ラムダ式が用意されています。
例えば:

Python
increment = lambda x: x + 1
Scala
def increment = (x:Int) => x + 1

ところが、R には、ラムダ式が用意されておらず、次のようにして無名関数を生成します。

R
increment <- function(x) x + 1

今回、R にラムダ式を導入するための関数 lambda() を作成してみました。
次のコードを実行してみて下さい。

R
install.packages("lazyeval") # インストールしていない人のみ
library(lazyeval)
lambda <- function(..., envir = parent.frame()) {
  if(!require(lazyeval)) stop("Please install.packages('lazyeval')")
  args <- lazyeval::lazy_dots(...)
  if(length(args) == 0) return(function() {})
  args <- Map(function(x) x$expr, args)
  vars <- unlist(Map(function(x) deparse(x), args[-length(args)]))
  expr <- as.character(args[length(args)])
  expr <- strsplit(expr, ":")[[1]]
  var <- expr[1]
  expr <- paste0(expr[-1], collapse=":")
  vars <- ifelse(is.null(vars), 
                 var, 
                 paste(paste(vars, collapse=","), var, sep=","))
  fun_str <- sprintf("function(%s) %s", vars, expr)
  eval(parse(text=fun_str), envir = envir)
}

これで関数 lambda() により、下記のように無名関数を生成できるようになりました。

R
increment <- lambda(x: x+1)
print(increment)
increment(1:5)
結果
function(x) x + 1
 2 3 4 5 6
R
add <- lambda(x,y: x+y)
print(add)
add(1:5, 6:10)
結果
function(x,y) x + y
 7  9 11 13 15

従来の書き方とラムダ式を使った書き方を比較してみましょう。

R
add <- function(x,y) x + y  #従来の書き方
add <- lambda(x,y: x + y)   #ラムダ式を使用

多少はコードが短くなったようですが、ラムダ式のありがたみが分かりますか?
実は、ラムダ式は、関数を引数にとる関数(高階関数)と組み合わせることで実力を発揮します。

実例として、R のための関数型プログラミング用の関数を lambda() 関数を使って改良してみましょう。
例として Filter(), Map(), Reduce() を改良します。
これらの関数については下記を参照してください。

まずは Filter() です。

R
Filter_ <- function(data, ...) {
  FUN <- lambda(..., envir = parent.frame())
  Filter(FUN, data)
}

このようにラムダ式を使えるような新たな関数を用意することで、次のような書き方が可能になります。

R
data = 1:10
Filter(function(x) x %% 2 == 0, data) #従来の書き方
Filter_(data, x: x %% 2 == 0)         #ラムダ式を使用
結果
 2  4  6  8 10

続いて Map() も同じように変更します。

R
Map_ <- function(data, ...) {
  FUN <- lambda(..., envir = parent.frame())
  Map(FUN, data)
}
R
library(dplyr)
data = 1:10
Map(function(x) x ** 2, data) %>% unlist #従来の書き方
Map_(data, x: x ** 2) %>% unlist         #ラムダ式を使用
結果
  1   4   9  16  25  36  49  64  81 100

さらに Reduce() に対してもラムダ式版を作ります。

R
Reduce_ <- function(data, ..., init, right = FALSE, accumulate = FALSE) {
  FUN <- lambda(..., envir = parent.frame())
  Reduce(f=FUN, x=data, init=init, right=right, accumulate=accumulate)
}
R
data = 1:10
Reduce(function(x,y) x + y, data) #従来の書き方
Reduce_(data, x,y: x + y)         #ラムダ式を使用
結果
 55

どうでしょう?
ラムダ式のありがたみはまだ分かりませんか?

それではこれらを組み合わせたコードを書いてみましょう。

R
#従来の書き方
data <- 1:10
fil <- Filter(function(x) x %% 2 == 0, data)
map <- Map(function(x) x**2, fil)
res <- Reduce(function(x,y) x+y, map)
print(res)
R
#ラムダ式を使用
library(dplyr)
1:10 %>% Filter_(x: x %% 2 == 0) %>% Map_(x: x**2) %>% Reduce_(x,y: x+y)
結果
 220

従来の書き方ではごちゃごちゃしていたコードが、めちゃめちゃすっきりしました!

ラムダ式と dplyr のパイプ %>% は非常に親和性が高く、相乗効果によりこれだけすっきりしたコードが書けるようになったのです。

どうでしょうか?
慣れないうちは、このコードは読みにくいかもしれません。
しかし、関数型プログラミングに慣れている人にとっては、R でもこのような書き方ができるということに、大きなメリットを感じるのではないでしょうか。

以上です。

追記

Map_() 関数が1変数にしか対応していないので2変数以上用の関数を作成してみました。

R
Map2_ <- function(data, ...) {
  FUN <- lambda(...)
  args <- c(f=FUN, data)
  do.call(Map, args)
}
list(1:10, 2:11) %>% Map2_(x,y: x+y) %>% unlist
list(1:10, 2:11, 3:12) %>% Map2_(x,y,z: x+y+z) %>% unlist
結果
 3  5  7  9 11 13 15 17 19 21
 6  9 12 15 18 21 24 27 30 33

どうにかして共通化したいですけど、無理ですかねー。

関連記事

参考

18
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
20