R言語 (5)

scheme by haskell

前回はRをgdbした。そしたら、CADR とかそれっぽいのが出てきた。んな事で、少し遊んでみる。

久しぶりにhaskellさんの所へ行ったら、48時間でschemeを作りましょに刺激されて、新しいのを作ったぞワァーイってのが陳列されててた。

Write You A Scheme, Version 2

stackが全て面倒見てくれるから、cabal地獄は心配しなくていいよとこ事。なぞるだけなんだけど、Fork me on github のリンクを辿って、一式取り寄せ。

./build って叩くと、stackがちょっと古びたghcを取ってきてコンパイルを始めてくれる。しばし時間がかかるので、お茶でもしながら待とう。

インストール中にpandocなんてモジュールを用意してたぞ。何処かに聞いた名前だってんで、調べると

多様なフォーマットに対応!ドキュメント変換ツールPandocを知ろう

なる程、文法解析をhaskellのライブラリィに任せて、自在に文書フォーマット変換が出来るのね。正しいHaskellの使い方だな。

sakae@pen:~/hs-scheme$ stack exec docs
# pandoc (for output/docs/home.wiki)
# pandoc (for output/docs/10_conclusion.wiki)
   :
# pandoc (for output/docs/00_overview.wiki)
# pandoc (for output/scheme.html)
Build completed in 0:02m

何となく変換してるっぽい。それよりdebugモードで走らせろ。

sakae@pen:~/hs-scheme$ stack repl   

 * * * * * * * *
The main module to load is ambiguous. Candidates are: 
1. Package `scheme' component scheme:exe:docs with main-is file: /home/sakae/hs-
scheme/Build.hs
2. Package `scheme' component scheme:exe:scheme with main-is file: /home/sakae/h
s-scheme/exec/Main.hs
You can specify which one to pick by:
 * Specifying targets to stack ghci e.g. stack ghci scheme:exe:docs
 * Specifying what the main is e.g. stack ghci --main-is scheme:exe:docs 
 * Choosing from the candidate above [1..2]
 * * * * * * * *

Specify main module to use (press enter to load none): 2 
Loading main module from candidate 2,
 --main-is /home/sakae/hs-scheme/exec/Main.
  :
[7 of 7] Compiling Main             ( /home/sakae/hs-scheme/exec/Main.hs, interpreted )
Ok, modules loaded: Repl, Parser, LispVal, Eval, Cli, Prim, Main.
Loaded GHCi configuration from /tmp/haskell-stack-ghci/54e11cc9/ghci-script
*Main Cli Eval LispVal Parser Prim Repl> :browse Eval
basicEnv :: EnvCtx
safeExec :: IO a -> IO (Either String a)
runASTinEnv :: EnvCtx -> Eval b -> IO b
evalFile :: FilePath -> Data.Text.Internal.Text -> IO ()
fileToEvalForm ::
  FilePath -> Data.Text.Internal.Text -> Eval LispVal
runParseTest :: Data.Text.Internal.Text -> Data.Text.Internal.Text
getFileContents :: FilePath -> IO Data.Text.Internal.Text
textToEvalForm ::
  Data.Text.Internal.Text -> Data.Text.Internal.Text -> Eval LispVal
evalText :: Data.Text.Internal.Text -> IO ()
*Main Cli Eval LispVal Parser Prim Repl>  :browse Repl
mainLoop :: IO ()
*Main Cli Eval LispVal Parser Prim Repl> mainLoop
Repl> (cdr '(a b c))
(b c)

mainが2つ有るから選べと言われて、schemeを選ぶと、コンパイルされて、モジュールがロードされた。で、Evalのモジュールを確認。それからReplのモジュール内の関数を調べ、それを起動したら、やっとユーザーが使えるreplが顔を出したとな。

*Main Cli Eval LispVal Parser Prim Repl>  :browse LispVal
data EnvCtx = EnvCtx {env :: LispVal.ValCtx, fenv :: LispVal.FnCtx}
type role Eval nominal
newtype Eval a
  = Eval {unEval :: Control.Monad.Trans.Reader.ReaderT EnvCtx IO a}
data LispVal
  = Atom Data.Text.Internal.Text
  | List [LispVal]
  | Number Integer
  | String Data.Text.Internal.Text
  | Fun IFunc
  | Lambda IFunc EnvCtx
  | Nil
  | Bool Bool
data IFunc = IFunc {fn :: [LispVal] -> Eval LispVal}
showVal :: LispVal -> Data.Text.Internal.Text
data LispException
  = NumArgs Integer [LispVal]
  | LengthOfList Data.Text.Internal.Text Int
  | ExpectedList Data.Text.Internal.Text
  | TypeMismatch Data.Text.Internal.Text LispVal
  | BadSpecialForm Data.Text.Internal.Text
  | NotFunction LispVal
  | UnboundVar Data.Text.Internal.Text
  | Default LispVal
  | PError String
  | IOError Data.Text.Internal.Text

同じ要領で、Lispの値を確認。値一般、関数の詳細、表示方法、各種の例外ってな事になってる。

rtags

/usr/lib/R/bin/rtags なんて言う、思わせぶりなコマンドが置いてあった。

## rtags -- tag source files recursively in a directory tree
##
## Examples:
##  R CMD rtags -o TAGS /path/to/Rsrc/

冒頭に使い方の説明が出てた。C語、R語、Rdファイルに対する、TAGを作ってくれるようだ。

sakae@pen:~$ R CMD rtags -o TAGS /tmp/R-4.0.2/src/

Tagging R/C/Rd files under /tmp/R-4.0.2/src/; writing to TAGS (overwriting)...

etags: no input files specified.
        Try 'etags --help' for a complete list of options.
etags: no input files specified.
        Try 'etags --help' for a complete list of options.
etags: no input files specified.
        Try 'etags --help' for a complete list of options.
etags: no input files specified.
        Try 'etags --help' for a complete list of options.
Done

何だろう、このエラー。

sakae@pen:/tmp/R-4.0.2/src$ find . -name paste.R
./library/base/R/paste.R
sakae@pen:/tmp/R-4.0.2/src$ grep paste.R TAGS
/tmp/R-4.0.2/src/library/base/man/paste.Rd,56

マニュアル用のRdは検出してるけど、R語のやつは無いな。ソースを読んで、余り難しい事はやらない事にした。

sakae@pen:/tmp/R-4.0.2/src$ R CMD rtags

Tagging R/C/Rd files under /tmp/R-4.0.2/src; writing to TAGS (overwriting)...

etags: no input files specified.
        Try 'etags --help' for a complete list of options.
  :
sakae@pen:/tmp/R-4.0.2/src$ grep paste.R TAGS
/tmp/R-4.0.2/src/library/base/R/paste.R,32
/tmp/R-4.0.2/src/library/base/man/paste.Rd,56

今度は大丈夫。

でも疑問、 素のetagsはR語を知ってるのか? 知らないよな。なら、秘密はスクリプト自身に隠されているはず。丁寧に見て行ったら、

if ${rfiles}; then
    SCRIPT_DIR=$(dirname $0)
    echo "

require(utils)

rtags(path.expand('${SRCDIR}'), pattern = '[.]*\\\\\.[RrSs]$',
      keep.re = '/R/[^/]*\\\\\.[RrSs]',
      verbose = ${VERBOSE},
      ofile = '${ofile}',
      append = ${APPEND},
      recursive = TRUE)" | "${RPROG}" --slave

fi

shellスクリプトの中から、Rにパイプしてる。Rが動いた事を悟られないように、静かに実行してねってモードに指定。なる程ね。

An Etags-like Tagging Utility for R

Description:

     ‘rtags’ provides etags-like indexing capabilities for R code,
     using R's own parser.

Rの事はRに任せておけ。正しい使い方ですなあ。それから、etagsでファイルが見つからないって警告は、*.hppとかを検索対象に(安全の為に)含めているからだな。あー、すっきりした。

Rprof

rtagsと同列に、便利スクリプトが陳列されてた。そのうちで興味を引いたやつ。

# ${R_HOME}/bin/Rprof -*- sh -*- for processing Rprof() files

args=
while test -n "${1}"; do
  args="${args}nextArg${1}"
  shift
done

## NB: Apple's ICU needs LC_COLLATE set when R is started.
echo 'tools:::.Rprof()' | R_DEFAULT_PACKAGES=utils LC_COLLATE=C "${R_HOME}/bin/R" --vanilla --slave --args ${args}

ファイルの冒頭がシェバングかと思ったら、微妙に違う。あくまでもRを起動しておいて、そこから動かせってスタンス。

> Rprof( filename = "Rprof.out", append = FALSE, interval = 0.02, memory.profiling = TRUE )
> source("ana.R")
> main()
> Rprof(NULL)

repl上で、こんな風にプロファイリングする。repl中に結果を見るなら、

summaryRprof( filename = "Rprof.out", memory = "both" )

とかすれば良い。後でじっくり見たいなら

debian:tmp$ R CMD Rprof

Each sample represents 0.02 seconds.
Total run time: 0.68 seconds.

Total seconds: time spent in function and callees.
Self seconds: time spent in function alone.

   %       total       %        self
 total    seconds     self    seconds    name
  94.1      0.64       0.0      0.00     "main"
  61.8      0.42       0.0      0.00     "mkgr"
  58.8      0.40       0.0      0.00     "doTryCatch"
  58.8      0.40       0.0      0.00     "tryCatch"
  58.8      0.40       0.0      0.00     "tryCatchList"
   :
   2.9      0.02       0.0      0.00     "vctrs::vec_as_names"
   2.9      0.02       0.0      0.00     "vec_as_subscript"

   %        self       %      total
  self    seconds    total   seconds    name
  17.6      0.12      17.6      0.12     ".External"
  14.7      0.10      14.7      0.10     "lazyLoadDBfetch"
   8.8      0.06      14.7      0.10     "exists"
   5.9      0.04       5.9      0.04     "cmpSymbolAssign"
   :
   2.9      0.02       2.9      0.02     "scan"
   2.9      0.02       2.9      0.02     "structure"

こんな感じでおk。

debian:tmp$ strace -p 4852 -c
strace: Process 4852 attached
^Cstrace: Process 4852 detached
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 40.48    0.000017           0       182           rt_sigaction
 19.05    0.000008           0        13           _newselect
 16.67    0.000007           0        57           write
  7.14    0.000003           0         9           pselect6
  4.76    0.000002           0         9           read
  4.76    0.000002           0        20           ioctl
  4.76    0.000002           0        36           rt_sigprocmask
  2.38    0.000001           0         2           clock_gettime
  0.00    0.000000           0         4           getrusage
  0.00    0.000000           0         1           mmap2
------ ----------- ----------- --------- --------- ----------------
100.00    0.000042                   333           total

リナ系だとstraceをRにアタッチさせて、挙動を調べる方法も有るとな。これ、応用範囲が広そう。ハスけるは、裏で何に時間を喰っているかとかね。簡単に暴けるぞ。

tidyverse

R言語ではじめるプログラミングとデータ分析 なんて本の案内を見ていたら、Tidyverse なんてのを利用してた。既存のやつと何が違うの?

Tidyverse packages 色々なものの詰め合わせセットなのね。

有名らしくて、色々な人が注目してた。そのうち、興味を引いたのは、こちら。 R: tidyverseをふつうに使えるようになる(purrr) (膨大な記事があって、楽しい)

{purrr} mapを導入しよう やっとmapが出てきたんか。foldとかは、どうよ。別の名前で登場してるか。

R -d gdb

面白い物を見つけた。 R本体Cの関数をデバッグする(その1)

再現実験

paste0('hoge', 'fuga') を、 do_paste で、捉えてみた。まずは、構造。

(gdb) p R_inspect(args)
@16f98a8 02 LISTSXP g0c0 []
  @16f93a4 19 VECSXP g0c1 [] (len=2, tl=0)
    @16f947c 16 STRSXP g0c1 [NAM(3)] (len=1, tl=0)
      @16f94e8 09 CHARSXP g0c1 [gp=0x60] [ASCII] [cached] "hoge"
    @16f9410 16 STRSXP g0c1 [NAM(3)] (len=1, tl=0)
      @16f94a0 09 CHARSXP g0c1 [gp=0x60] [ASCII] [cached] "fuga"
  @8816b8 00 NILSXP g0c0 [MARK,NAM(3)]
$11 = (struct SEXPREC *) 0x16f98a8

そして、中身を確認。

(gdb) p Rf_PrintValue(args)
[[1]]
[[1]][[1]]
[1] "hoge"

[[1]][[2]]
[1] "fuga"


[[2]]
NULL

リスト構造が伺えるな。

gdb 起動の仕組み

例では、いきなり裸のgdbが起動したけど、仕組みはどうなってる? 自前でコンパイルしたRが、~/MINIE/bin に入れてあるので、追跡調査。

debian:~$ cd MINE/bin/
debian:bin$ sh -x R -d gdb
 :
+ exec gdb --args /home/sakae/MINE/lib/R/bin/exec/R
GNU gdb (GDB) 9.1
Copyright (C) 2020 Free Software Foundation, Inc.
  :
Reading symbols from /home/sakae/MINE/lib/R/bin/exec/R...
(gdb)

shellを -x でトレース。shの最後に Rの実体を引数にしてgdbを起動させてるのね。

(gdb) b main
Breakpoint 1 at 0x2c579: file Rmain.c, line 28.
(gdb) r
Starting program: /home/sakae/MINE/lib/R/bin/exec/R
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1".

Breakpoint 1, main (ac=1, av=0xbffff384) at Rmain.c:28
28          Rf_initialize_R(ac, av);

なる程、 Rf_xxx ってのは、ユーザーが使う関数( do_yyy )以外の、裏方仕事関数なんだな。

shのトレースで、重要そうな環境引数が列挙されてた。

+ R_HOME=/home/sakae/MINE/lib/R
+ LD_LIBRARY_PATH=/home/sakae/MINE/lib/R/lib:/usr/local/lib:/usr/lib/i386-linux-
gnu:/usr/lib/jvm/java-11-openjdk-i386/lib/client
+ R_binary=/home/sakae/MINE/lib/R/bin/exec/R

これらをexportしておいて、いきなりemacs 上で、M-x gdb して、

Run gdb (like this): gdb -i=mi $R_binary

こんな風に、gdbを動かせるぞ。

それから、こういうのも有る。 見えないRの関数のソースコードを読む

Rscript

Rscriptはバイナリーファイルになってる。Rとどういう関係なの?

debian:tmp$ echo 'print("Hello R")' > test.R
debian:tmp$ gdb -q Rscript
Reading symbols from Rscript...
(gdb) b main
Breakpoint 1 at 0x1120: file ./Rscript.c, line 124.
(gdb) r test.R
Starting program: /home/sakae/MINE/bin/Rscript test.R

Breakpoint 1, main (argc_=2, argv_=0xbffff444) at ./Rscript.c:124
124         if(argc_ <= 1) {
(gdb) c
Continuing.
process 6638 is executing new program: /bin/bash

Breakpoint 1, 0x0041fe40 in main ()
(gdb) c
Continuing.
[Detaching after fork from child process 6680]
[Detaching after fork from child process 6681]
process 6638 is executing new program: /home/sakae/MINE/lib/R/bin/exec/R
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1".

Breakpoint 1, main (ac=4, av=0xbffff314) at Rmain.c:28
28          Rf_initialize_R(ac, av);

ふむ、shで書かれたRの実体呼び出しに変わって、バイナリ版なのね。何故こうする必要が有る? スピードの問題?

src/unix/Rscript.c に答えは有るかな?

/* This is intended to be used in scripts like

#! /path/to/Rscript --vanilla
commandArgs(TRUE)
q(status=7)

This invokes R with a command line like
R --slave --no-restore --vanilla --file=foo [script_args]

*/

/* execv exists and can be used on Windows, but it returns immediately
   and so the exit status is lost.  HAVE_EXECV is defined under Windows.

   The main reason for using execv rather than system is to avoid
   argument quoting hell.
*/

Rでの引数受け取り方法が、地獄を見るって事?

etc