niszetの日記

アナログCMOS系雑用エンジニアが頑張る備忘録系日記

(R) felpパッケージとfelp関数の紹介(と、主に裏側の話)

TokyoRの打ち上げの時の何気ない会話から生まれた

今回は、Atsushiさんの手によって生み出されたfelpパッケージについて主にその裏側…というか見えない部分についてちょっと説明してみようと思います。

このパッケージの誕生の経緯についてはAtsuchiさんの以下のQiitaの記事に書かれていますので、そちらを読んでみてください。

qiita.com

なお、記事執筆当時と最新のパッケージとで関数や挙動に一部差異があります。最新のパッケージは問題なく動くはずですが、もし何か見つけたらIssue等でAtsushiさんに報告してみましょう

GitHubレポジトリは以下です

github.com

今回はこのパッケージの宣伝と、主に環境(environment)起因で色々と課題が出ていたので、その過程で私も色々と調べたのでそれらについて説明1します インストール方法はGitHubに、関数の中身の動作についてはQiitaの方を参照していただければよいかと思います。

動作の基本

さてこのfelpパッケージに含まれる関数felp()ですが、Rで関数について調べているときに関数のヘルプと同時にソースコードも見たい、なんてことありませんか?
引数や戻り値はヘルプで見れても、内部でエラーが出るいる場合、どこでその処理がされているかを関数の説明とともに見たいことはよくあると思います。

felpはそういう痒い所に手が届くパッケージとなっています。関数の中身を読むようになっているということは、使用者のターゲットは脱初心者している方といった感じですね。あ、でも、初心者からでもヘルプ読むの大事ですよ…本当に2
この関数はヘルプを表示するのと関数の内部を表示するために、関数内部ではhelp()print.function()が使われています。

パッケージ名も含めれば、それぞれの関数の出どころはutils::help()base::print.function()ですね。
このあたりの挙動についてはAtsushiさんのQiitaの記事の方に書かれていますので、そちらを参照してみてください。

この記事では書かれていないことについて書いていく形です。

print.function がうまく動かない問題

さて、このprint.function()関数は「関数を表示する」関数です。コンソール上で関数名に()をつけずに書いてEnterすると関数名の中身が表示されますよね。アレです。
実際、これを明示的に呼び出して、

base::print.function(help)

とすれば、この場合はhelp()関数の中身が見れるというわけですね。
ただ、この「コンソール上に関数名を書いてEnter」したときの挙動の正確な部分についてはこれよりもちょっと複雑で、詳細についてはid:yutannihilationさんの記事を参照してください。

notchained.hatenablog.com

私もこれを完全には理解できていませんが、RのREPLが新しい環境をつくって...という挙動らしいです3。 結果として、「コンソール(REPL)上で関数名を書いてEnter」して関数の中身を表示させるときと、print.function()で明示的に関数を呼び出す場合とではRの(裏側の)挙動が異なります。
関数の引数に関数名を受け取る4ような書き方をした場合、print.function()を明示的に呼び出せばちゃんと動作しますが、「コンソール上で関数名を書いてEnter」の場合は引数に与えた関数名を受け取ることが出来ない、ということが起きたのでした。

直前に入力された関数の名前を取得したい

さて、そんなわけで「コンソール上に関数名を書いてEnter」したときであっても5正常に動作させるには「関数の引数として関数名を受け取る」以外の方法が必要となったようです。
最終的にfelpパッケージでは「一度入力されたコマンドをファイルに保存しておいて、必要なタイミングで読みだす」ことで直前に入力された関数名を取得することになったようなのですが・・・

そもそもRには入力履歴を直接取り出すような関数はなさそうです6
そこで、コマンド入力履歴(history)を一旦ファイルに保存し、それの最終行を読む(直前に入力された関数名が入る)という方法がfelpパッケージではとられています。このhistoryはRStudio IDEでは右上(デフォルト設定時)にあるHistory Paneが相当しますね。

savehistoryの挙動

さて、ここで一度壁にぶつかっていたのでちょっと参戦していました。こちらの記事、

niszet.hatenablog.com

にあるように、コマンドヒストリを保存する関数、savehistory()はなんとRStudio IDEによって上書きされています。7しかもコレはWindows上でRStudio IDEで実行したときのみ起こる問題で、しかもutils::savehistory()Windows上のRStuido IDE上で実行してもエラーは出ず、しかしファイル自体も出力されないということで混乱を招いていたのでした。

総称関数はexportされている関数だけのものか?

結果として、S3の総称関数はexportされていないものであっても正常に動作する、プラットフォーム(OS)に依存しない、という結果です。上記のsavehistoryの挙動とあわせて一時「exportしないと使えないのでは…プラットフォームに依存するのでは…baseパッケージは特別なのでは…」という話がありましたが、たぶんどれも思い過ごしであったかなと。(認識違いあればツッコミください)

savehistoryがなぜ上書きされているのか?

さて、そもそもなぜこの関数はwindows上でのみ上書きされているのでしょうか?RStudioの方のソースコードを読んでも特にコメントなどは書いてありませんでした。

結果として、これは素のRのコードを読むしかありませんでした。Rのversion 3.4.3のソースをダウンロードして、がんばって目検…。すると、それらしき関数はsrcディレクトリ中のstubs.cにありました。

#ifdef Win32
# include "Startup.h"
# include "getline/getline.h"     /* for gl_load/savehistory */
# include "getline/wc_history.h"  /* for wgl_load/savehistory */
SEXP savehistory(SEXP call, SEXP op, SEXP args, SEXP env)
{
    SEXP sfile;

    args = CDR(args);
    sfile = CAR(args);
    if (!isString(sfile) || LENGTH(sfile) < 1)
    errorcall(call, _("invalid '%s' argument"), "file");
    if (CharacterMode == RGui) {
    R_setupHistory(); /* re-read the history size */
    wgl_savehistoryW(filenameToWchar(STRING_ELT(sfile, 0), 0), 
             R_HistorySize);
    } else if (R_Interactive && CharacterMode == RTerm) {
    R_setupHistory(); /* re-read the history size */
    gl_savehistory(translateChar(STRING_ELT(sfile, 0)), R_HistorySize);
    } else
    errorcall(call, _("'savehistory' can only be used in Rgui and Rterm"));
    return R_NilValue;
}

…。
まぁ、細かい動作はとりあえず置いておいて、RGuiとRTermという文字が見えます。errorcallという関数のメッセージから、どうやらこれらの環境以外では動作しないようです。

RGuiとかRTermって何?RStudioとは違うの?

さて、windows上で下記のコードをコンソール上で実行してみてください。

base::.Platform$GUI

RStudio IDE上で動作させている人は

#> [1] "RStudio"

が返ってくると思います。emacsとかでどうなるかはわかりません…あとmacとかlinuxも。面白い結果が出たら教えてください。

素のRを実行している人は同じ入力で

#> [1] "RTerm"

が返ってくると思います。もし、Rへのパスが通っていない場合は、R3.4.3であれば下記のディレクトリの中を見てみてください。R.exeとRgui.exeがあると思います。例えばこの辺り。

C:\Program Files\R\R-3.4.3\bin\x64

この場所にあるRgui.exeを実行し、同様に上記の関数を入力して実行すると…

f:id:niszet:20180311192809p:plain

#> [1] "Rgui"

と、得られます。ということで、これらの実行環境がRTermとRguiが意味する動作環境ということですね。

RStudioでもhistory自体は使えるのだから、これを使えれば良いのでは…?

先ほど書いたように、実際、RStudio IDEは右上(デフォルト設定)にHistory Paneがあるわけで、機能自体はあるわけです。

さて、これが一筋縄ではいかない点でして…。 ひとまずfelpからの親環境を追ってみるです。

View(rlang::env_parents(felp::felp))

f:id:niszet:20180311193719p:plain

こんな感じ。念のためenviroinment(felp::felp)も観てみると、 f:id:niszet:20180311194104p:plain

です。が、結果としてfelpの環境自体はあまり関係ない。

さて、親環境を見てみると、imports:felpnamespace:baseR_GlobalEnvとの間にあることが分かりますね。そのため、パッケージ内から普通に関数名で検索しにいくとimportsかbaseの階層でsavehistory()が見つかってしまうという理解です。つまり、これを飛び越えてしまえば良いのですね。

さて一体どうするのか?…というと、

こうじゃ!

get("savehistory", envir=as.environment("tools:rstudio"))
#> function (...) 
#> .rs.callAs(name, hook, original, ...)
#> <environment: 0x0000000015618bd0>

表示されている文字はコンソール上でsavehistoryと書いたときと一致することが確認できます。

先の私の記事にあるように、この環境(environment)がRStudio IDEが作った環境で、ここでオリジナルの関数が書き換えられています。そこで、この環境をas.environment("tools:rstudio")で取得し、get()でsavehistory関数名を指定してget()してあげれば得られるという仕組みです。

これは、RStudio IDEによって上書きされている他の関数、View()なども同じです。

library(tuneR)などでパッケージをアタッチしても、その環境はグローバル環境とこのas.environment("tools:rstudio")で示される環境の間に入ります。

f:id:niszet:20180311200301p:plain

…こんな感じ。
そのため、このas.environment("tools:rstudio")が示す環境を起点にしておけば、同名の関数が仮に別のライブラリに含まれていてもfelpの挙動は変わらないということになります(多分)。グローバル環境を起点にしても動くはずですが、こちらの方が安全に動くことがわかりますね。

この方法はちょっとトリッキーであまり多用するべきではないとは思いますが、環境as.environment("tools:rstudio")がパッケージ由来ではなくプラットフォームによって挙動が変わるという仕様のため避けられないなと思います。

本当の本当の挙動はどうなってるの?

この環境にはsavehistoryなる関数が定義されていないことは調べればわかるのですが、上に書いたような方法でなぜ関数名が見つかってしまうのかはちょっとまだわかっていないです。
ただ、おそらくinvokeHookによって関数を探しているのだと思っています。↓このあたり。

github.com

まだまだ開発は続く…はず

そんなわけで、felp内部の主に黒魔術的な部分の解説でした。この記事必要なのだろうか謎8
このあたりを全然知らなくてもfelpパッケージ、関数は使えますので、是非パッケージをインストールして遊んでみてください(宣伝)

ちなみに、

felp::felp(help) %>% View()

のようにパイプで渡すことも出来るようです。これも結構便利ではないかな。
是非是非使ってみて、Atsushiさんにフィードバックしていってみてください。

というわけで。

Enjoy!!


  1. …なんて言っても、ワタシ大したこと書けないですけど…。分かったことだけ書いていきますね。

  2. ほんとう。

  3. RのCで書かれたソースを読むのは非常にしんどいので…魂が持っていかれる…。タブとホワイトスペースの混在したソースコード、ダメ、絶対。

  4. help(help)みたいな使われ方を想像してください

  5. わざわざprint.function()で関数のソースを見ている人って多分いないのでは…ということで、これで動作するの大事。

  6. より正確には「ありそうだけどRのC実装を頑張って読み解くしかない…つらい」ので事実上無理という感じですね。

  7. コンソール上でsavehistoryと書いてEnterすると、その関数が存在する環境/パッケージが表示されます。utilsではないことがわかると思います。

  8. 魅力を伝えられる記事になっているのかという意味で。