RSRubyをちょっとだけ拡張してRっぽく書く


(追記 2020.8.19) 
最新版であるrsruby 0.5.1.1はRuby2.7では動かなくなっています。Ruby2.6またはそれ以前のバージョンで使用ください。

Rubygemsにrsrubyが登録されていて、RubyからRを使うことができます。
それはそれで十分素晴らしいのですが、書き方があまりRっぽくないのです。

例えば、Rで書くとこうなります。

```R:t_test.R
e <- t.test(c(1,2,3,4,5,6))
print(e)
print(e$statistic['t'])
```

rsrubyで書くと(rsrubyのexampleにある)

```Ruby:t_test0.rb
require 'rsruby'
require 'rsruby/erobj'

r=RSRuby.instance
r.proc_table[lambda{|x| true}] = lambda{|x| ERObj.new(x)}
RSRuby.set_default_mode(RSRuby::PROC_CONVERSION)

e = r.t_test([1,2,3,4,5,6])
puts e
puts e.statistic['t']
```

結構ごちゃごちゃしています。
以下に掲載しているrsruby_ext.rbを使って、言語内DSLっぽくするとこんなに感じに書くことができます。

```Ruby:t_test.rb
require './rsruby_ext'

RSRuby.execute do
	e = t_test([1,2,3,4,5,6])
	print_ e
	print_ e.statistic['t']
end
```

かなり手抜きしていますが公開しておきます。自由にお使いください。

```Ruby:rsruby_ext.rb
# version 0.1.2
require 'rsruby'
require 'rsruby/erobj'

$rs=RSRuby.instance
class RSRuby
    def self.execute(&block)
        RSRuby.set_default_mode(RSRuby::NO_CONVERSION)
        block.arity < 1 ? $rs.instance_eval(&block) : block[$rs]
    end
end

class RObj
    def inspect() $rs.str(self) end
    def to_erobj() ERObj.new(self) end
    def ==(arg) $rs[:==].call(self, arg) end
    def !=(arg) $rs[:!=].call(self, arg) end
    def [](args)
    	case args
    	when Array then $rs['[['].call(self, *args)
    	when Range then $rs['['].call(self, $rs[':'].call(args.first, args.last-(args.exclude_end? ? 1 : 0)))
    	else $rs['['].call(self, *args)
    	end
    end
    
    def to_i() self.to_erobj.to_ruby.to_i end
    def to_f() self.to_erobj.to_ruby.to_f end
	def to_hash() Hash[self.to_erobj.to_ruby] end
	def to_s() self.to_erobj.to_ruby end
    def to_a
        case $rs.length(self).to_i
        when 0 then []
        when 1 then [self.to_erobj.to_ruby]
        else self.to_erobj.to_ruby
        end
    end 
    
    def method_missing(method_name, *args)
        begin
            return $rs[method_name].call(self, *args)
        rescue  RException
        end
        if args.empty?
            begin
                return $rs['$'].call(self, method_name.to_s)
            rescue  RException
            end
        end
        super
    end
end
```

## 使い方
基本的にはRSRubyの文法に従います。RSRubyのmanual.pdfがリンク切れになっているようです。
[github alexgutteridge/rsruby](https://github.com/alexgutteridge/rsruby)のmanual.texがそれなのですが、拡張部分も含めて以下に書いておきます。

まずは、rsruby_extの拡張部分

```ruby1
require './rsruby_ext' # rsruby_ext.rbを読み込み

RSRuby.execute do
	# この間に書いたプログラムは、Rubyで実行されるが、
	# Rubyに無いメソッドはRの関数として実行される。
end
```

次のようにブロックが分かれていても構いません。最後の式の評価がブロックの値として返ります。

```Ruby:ruby2
RSRuby.execute do
	# 処理(Ruby -> R)
end
# 処理(Ruby)
RSRuby.execute do
	# 処理(Ruby -> R)
end
```

ブロックの中のメソッドは、Rubyのメソッドが優先されます。言い換えると、RubyとRと同じ名前の場合にはRubyとして実行され、RubyになければRの関数として実行されます。printのように同名の場合はprint_のように後ろに_を追加すると、Rのprintとして実行されます。

ブロックパラメータがある場合には、次のように書くことができます。

```Ruby:ruby3
RSRuby.execute do |r|
	x=r.data_frame(x: [1,2,3], y: [5,6,7])
end
```



###サインカーブをかいてみる
Rだと、

```R
x<-seq(0, 360, by=5)*(pi/180.0)
data<-data.frame(x=x, y=sin(x))
print(data)
plot(data)
```

のようになります。

そのままRubyにするとこんな感じに、

```Ruby4
require './rsruby_ext'

RSRuby.execute do
	x=seq(0, 360, by: 5)*(pi/180.0)
	data=data_frame(x: x, y: sin(x))
	print_ data
	plot(data)
end
sleep 10
```

`x=seq(0, 360, by: 5)*(pi/180.0)`
seqとpiはRubyにはありませんので、Rの関数として実行されます。
結果xにはRObjのインスタンスが代入されます。

`data=data_frame(x: x, y: sin(x))`
Rのdata.frameのようにドットが関数名に含まれている場合には下線に置き換えておきます。Rの関数の引数は、Rubyの数値や配列を使うこともできますが、RObjであっても構いません。rsrubyのデフォルトでは可能な限りRubyオブジェクトに変換していましたが、rsruby_extではRObjのまま代入するようにしています。

`print_ data`
RubyとRの両方にprintメソッドがありますので、Rの関数として実行したい時には後ろに下線を追加します。

元のRSRubyから拡張されているところは

1. +や*などの演算子をそのまま使うことができます。
2. Rのdata$xはdata.xのように書きます。
3. data[1]やdata["x"]は同じように書くことができます。ただし、Rは添え字は1から始まりますので、0だとエラーになります。

###Rubyのインスタンスに変換するには
to_i
to_f
to_a
to_hash
to_s
to_eobj.to_ruby
を使ってください。

## RとRSRuby_extの比較表

###ベクトル
|R|RSRuby_ext|
|:-:|:-:|
|v1=c(0.1, 0.2, 0.3, 0.4)|←|
|v1[2]|←|
|v1[[2]]|←|
|v1[2:3]|v1[2..3]|
|v1*2|←|


###ベクトル(属性名付きでの追加)
|R|RSRuby_ext|
|:-:|:-:|
|v2=c(a=0.1, b=0.2, c=0.3, d=0.4)|v2=c(a: 0.1, b: 0.2, c: 0.3, d: 0.4)|
|v2["b"]|←|
|v2[["b"]]|←|
|v2$b|v2.b|


###リスト
|R|RSRuby_ext|
|:-:|:-:|
|list1=list(c(0.1,0.15), 0.2, 0.3, 0.4)|list1=list([0.1,0.15], 0.2, 0.3, 0.4)|
|list1[1]|←|
|list1[1:2]|list1[1..2]|
|list1[[1]]|←|


###リスト(属性名付きでの追加)
|R|RSRuby_ext|
|:-:|:-:|
|list2=list(a=c(0.1,0.15), b=0.2, c=0.3, d=0.4)|list2=list(a: [0.1,0.15], b: 0.2, c: 0.3, d: 0.4)|
|list2["a"]|←|
|list2[["a"]]|←|
|list2$a|list2.a|

###データフレーム
|R|RSRuby_ext|
|:-:|:-:|
|df1=data.frame(x=c(0.1,0.2,0.3,0.4), y=c(-2,-3,-4,-5))|df1=data_frame(x: [0.1,0.2,0.3,0.4], y: [-2,-3,-4,-5])|
|df1[1]|←|
|df1[1:2]|df1[1..2]|
|df1["x"]|←|
|df1[["x"]]|←|
|df1$x|df1.x|
|df1[,1]|×|
|df1[1,]|×|
|df1[1,1]|×|


RSRuby_extではデータの取り出しはできますが、v1[2]=2のように要素の一部を書き換えることはできません、

##ggplot2の場合
ggplot2は+演算子を拡張しているようで、そのままでは正常に動かすことができませんでした。
もう一つの問題は、geom_pointのように関数名に下線を含むものがあります。Rでは.(ドット)を含む関数も多く存在するためにrsrubyで_(下線)を.に変換する機能をがあります。残念ながらこの問題を回避しても+演算子は正常に動かないようです。

最終手段としてのeval_Rを使ってみましたら、ggplot2が見事に使えました。

```ruby:ruby5
require './rsruby_ext'

RSRuby.execute do
	library("ggplot2")
	x=seq(0, 360, 5)*(pi/180.0)
	data=data_frame(x: x, y: sin(x))
	assign("data2", data)
	eval_R("gp<-ggplot(data2, aes(x=x, y=y))+geom_point()")
	plot(gp)
end
sleep 10
```