はじめての xfun

さまざまな関数

Yihui Xie

2021-04-10

約20個のRパッケージを書いた後、異なるパッケージ間で使用するいくつかのユーティリティー関数が蓄積されていることに気付きました。 以前はこれらの内部ユーティリティー関数にアクセスするために邪悪なトリプルコロン ::: を使っていました。 今回の xfun では、これらの関数はエクスポートされ、さらに重要なことにドキュメント化されています。 闇雲に使うのではなく、太陽の下で使うのが良いでしょう。

このページでは、このパッケージに含まれる関数のサブセットの例を示します。関数の全リストは、ヘルプページ help(package = 'xfun') をご覧ください。 ソースパッケージはGithubで公開されています: https://github.com/yihui/xfun.

リストの部分一致はもういらない!

リストの部分一致には何度も悩まされてきました。例えば、x$a としたいのに要素 a がリスト x に存在しない場合、xabc が存在すれば x$abc という値が返ってきます。 厳密なリストとは、$ 演算子の部分的なマッチングを無効にしたリストです。 関数 xfun::strict_list()xfun::as_strict_list() はそれぞれ base::list()base::as.list() に相当し、常にストリクトリストとして返します。

library(xfun)
(z = strict_list(aaa = "I am aaa", b = 1:5))
## $aaa
## [1] "I am aaa"
## 
## $b
## [1] 1 2 3 4 5
z$a  # NULL (strict matching)
## NULL
z$aaa  # I am aaa
## [1] "I am aaa"
z$b
## [1] 1 2 3 4 5
z$c = "you can create a new element"

z2 = unclass(z)  # a normal list
z2$a  # partial matching
## [1] "I am aaa"
z3 = as_strict_list(z2)  # a strict list again
z3$a  # NULL (strict matching) again!
## NULL

同様に、attr() のデフォルトの部分的なマッチングも悩ましいものがあります。 関数 xfun::attr() は、単に attr(..., exact = TRUE) の短縮形です。

欲しい、もしくは欲しくない。 「おそらく欲しい」というのはありません。

人間の目に見える文字ベクトルの出力

Rが文字ベクトルを出力するとき、[1]のような添字や二重引用符、エスケープシーケンスなどに目が奪われるかもしれません。 文字ベクトルを “生”の形で見るには,cat(..., sep = '\n')を使うとよい。 関数 raw_string() は文字ベクトルを “raw” としてマークし,対応する印刷関数は cat(sep = '\n') を呼び出して文字ベクトルをコンソールに表示します。

library(xfun)
raw_string(head(LETTERS))
A
B
C
D
E
F
(x = c("a \"b\"", "hello\tworld!"))
[1] "a \"b\""       "hello\tworld!"
raw_string(x)  # this is more likely to be what you want to see
a "b"
hello   world!

テキストファイルの内容を印刷する

シンプルなラッパー関数である xfun::file_string() を書こうと決心する前、paste(readLines('foo'), collapse = '\n') を何度も使っていました。 この関数は raw_string() も利用しているので、副次的にコンソールでファイルの内容を見ることができます。 例えば

f = system.file("LICENSE", package = "xfun")
xfun::file_string(f)
YEAR: 2018-2021
COPYRIGHT HOLDER: Yihui Xie
as.character(xfun::file_string(f))  # essentially a character string
[1] "YEAR: 2018-2021\nCOPYRIGHT HOLDER: Yihui Xie"

ファイルのデータURIを取得する

ファイルはbase64_uri()によってbase64文字列にエンコードすることができます。 これは任意のファイルをHTML文書に埋め込むための一般的なテクニックです (これは xfun::embed_file()がすること であり、base64_uri()をベースにしています)。

f = system.file("LICENSE", package = "xfun")
xfun::base64_uri(f)
## [1] "data:text/plain;base64,WUVBUjogMjAxOC0yMDIxCkNPUFlSSUdIVCBIT0xERVI6IFlpaHVpIFhpZQo="

文字列にマッチして置換を行う

x = grep(pattern, x, value = TRUE); gsub(pattern, '\1', x) というコードを何度も入力した後、これらを一つの関数 xfun::grep_sub() にまとめました。

xfun::grep_sub('a([b]+)c', 'a\\U\\1c', c('abc', 'abbbc', 'addc', '123'), perl = TRUE)
## [1] "aBc"   "aBBBc"

ファイル内の文字列を検索・置換する

複数のファイルに含まれる文字列を検索・置換するための grepsed の適切な使い方を思い出せません。 私のお気に入りの IDE である RStudio にはまだこの機能がありません(現在開いているファイルの中でしか検索や置換ができません)。 そこで、Rの関数 gsub_files(), gsub_dir(), gsub_ext() を使って、ディレクトリ下にある複数のファイルの文字列を検索・置換するという、簡単で汚い実装をしてみました。 なお、ファイルはUTF-8でエンコードされていることを前提としています。 UTF-8を使用していない場合,我々は友人になることはできません。 真面目な話。

すべての関数は,1つのファイルに対して検索や置換を行う gsub_file() をベースにしています。

library(xfun)
f = tempfile()
writeLines(c("hello", "world"), f)
gsub_file(f, "world", "woRld", fixed = TRUE)
file_string(f)
hello
woRld

関数 gsub_dir() は非常に柔軟で、MIME タイプや拡張子によってファイルのリストを制限することができます。 例えば、テキストファイルの置換を行いたい場合には,gsub_dir(..., mimetype = '^text/') を使用することができます.

関数 process_file() は,ファイルを処理するより一般的な方法です.基本的には,ファイルを読み,そこに渡された関数で内容を処理し,テキストを書き戻します。

process_file(f, function(x) {
  rep(x, 3)  # repeat the content 3 times
})
file_string(f)
hello
woRld
hello
woRld
hello
woRld

警告: これらの機能を使用する前に、ファイルをバックアップするか、ファイルをバージョン管理していることを確認してください。 ファイルはその場で修正されます。バックアップやバージョン管理を行っていない場合、後悔することになります。

ファイル名の拡張子を操作する

関数 file_ext()sans_ext() は、tools の関数をベースにしています。 関数 with_ext() は、ファイル名の拡張子を追加したり、置き換えたりするもので、ベクトル化されています。

library(xfun)
p = c("abc.doc", "def123.tex", "path/to/foo.Rmd")
file_ext(p)
## [1] "doc" "tex" "Rmd"
sans_ext(p)
## [1] "abc"         "def123"      "path/to/foo"
with_ext(p, ".txt")
## [1] "abc.txt"         "def123.txt"      "path/to/foo.txt"
with_ext(p, c(".ppt", ".sty", ".Rnw"))
## [1] "abc.ppt"         "def123.sty"      "path/to/foo.Rnw"
with_ext(p, "html")
## [1] "abc.html"         "def123.html"      "path/to/foo.html"

絶対/相対パスを考えずに(プロジェクト内の)ファイルを探す

関数 proj_root() は、rprojrootパッケージにヒントを得て、プロジェクトのルートディレクトリを探そうとするものです。 現在は、RパッケージプロジェクトとRStudioプロジェクトのみをデフォルトでサポートしています。 rprojrootよりもはるかに洗練されていません。

関数 from_root()here::here() にヒントを得ていますが、絶対パスではなく、相対パス(proj_root()で見つけたプロジェクトのルートディレクトリからの相対パス)を返します。 例えば、docs/foo.Rmd のコードチャンク中の xfun::from_root('data', 'cars.csv') は、docs/data/ がプロジェクトのルートディレクトリの下にある場合、../data/cars.csv を返します。

root/
  |-- data/
  |   |-- cars.csv
  |
  |-- docs/
      |-- foo.Rmd

ファイルパスを考えるのが面倒な場合は、関数 magic_path() に不完全なパスを渡せば、ルートディレクトリのサブディレクトリの下で再帰的に実際のパスを探そうとします。 たとえば、ベースとなるファイル名だけを指定すると、 magic_path() はそのファイルをサブディレクトリの下で探し、 見つかった場合には実際のパスを返します。 デフォルトでは、カレントワーキングディレクトリからの相対パスを返します。 上記の例では、docs/foo.Rmdのコードチャンク内のxfun::magic_path('cars.csv')は、cars.csvがプロジェクト内でユニークなファイル名である場合には、../data/cars.csvを返します。 このファイルをプロジェクト内のどのフォルダにでも自由に移動させても、magic_path() はそれを見つけることができます。 プロジェクトを使ってファイルを管理していない場合は、magic_path() は、現在の作業ディレクトリのサブディレクトリの下でファイルを探します。

オペレーティングシステムの種類

一連の関数 is_linux(), is_macos(), is_unix(), is_windows() は、.PlatformSys.info() の情報を用いて、OSの種類をテストします(例)。

xfun::is_macos()
## [1] TRUE
xfun::is_unix()
## [1] TRUE
xfun::is_linux()
## [1] FALSE
xfun::is_windows()
## [1] FALSE

パッケージのロードとアタッチ

しばしば、ユーザーがスクリプトの最初に library() を複数回繰り返すことで、一連のパッケージをアタッチしているのを見かけます。 これは簡単にベクトル化することができ、関数 xfun::pkg_attach() がこの仕事をしてくれます。 例えば、以下のようになります。

library(testit)
library(parallel)
library(tinytex)
library(mime)

これは、以下と同等です。

xfun::pkg_attach(c('testit', 'parallel', 'tinytex', 'mime'))

また、あるパッケージが利用できない場合に、そのパッケージをインストールするコードを含むスクリプトも見かけます。

if (!requireNamespace('tinytex')) install.packages('tinytex')
library(tinytex)

これは、以下を経由して達成されます。

xfun::pkg_attach2('tinytex')

関数 pkg_attach2()pkg_attach(..., install = TRUE) の短縮形で、パッケージが利用できない場合にインストールすることを意味しています。 この関数は、複数のパッケージを扱うこともできます。

関数 loadable() は、パッケージがロード可能かどうかをテストします。

ファイルをUTF-8で読み書きする

関数 read_utf8()write_utf8() を使うと、UTF-8でファイルを読み書きできます。これらは readLines()writeLines() の単純なラッパーです。

数字を英単語に変換する

関数 numbers_to_words()(略してn2w())は、数値を英単語に変換する。

n2w(0, cap = TRUE)
## [1] "Zero"
n2w(seq(0, 121, 11), and = TRUE)
##  [1] "zero"                       "eleven"                    
##  [3] "twenty-two"                 "thirty-three"              
##  [5] "forty-four"                 "fifty-five"                
##  [7] "sixty-six"                  "seventy-seven"             
##  [9] "eighty-eight"               "ninety-nine"               
## [11] "one hundred and ten"        "one hundred and twenty-one"
n2w(1e+06)
## [1] "one million"
n2w(1e+11 + 12345678)
## [1] "one hundred billion, twelve million, three hundred forty-five thousand, six hundred seventy-eight"
n2w(-987654321)
## [1] "minus nine hundred eighty-seven million, six hundred fifty-four thousand, three hundred twenty-one"
n2w(1e+15 - 1)
## [1] "nine hundred ninety-nine trillion, nine hundred ninety-nine billion, nine hundred ninety-nine million, nine hundred ninety-nine thousand, nine hundred ninety-nine"

R式をRDSファイルにキャッシュする

関数 cache_rds() は、シンプルなキャッシュメカニズムを提供します。 式が最初に渡されたときに、その結果を RDS ファイルに保存し、次回は式を再度評価する代わりに RDS ファイルを読み込んでその値を返します。 キャッシュを無効にしたい場合には、引数として rerun = TRUE を使用します。

res = xfun::cache_rds({
  # pretend the computing here is a time-consuming
  Sys.sleep(2)
  1:10
})

この関数を knitr ドキュメントのコードチャンク内で使用すると、RDSキャッシュファイルはチャンクラベル(ベースファイル名)とチャンクオプションの cache.path (キャッシュディレクトリ)で決まるパスに保存されるので、cache_rds()filedir の引数を指定する必要はありません。

このキャッシュの仕組みは knitr のキャッシュよりもはるかにシンプルなものです。 キャッシュの無効化はしばしば厄介です (this post を参照してください) ので、キャッシュを無効にするタイミングをより透明性を持って制御したい場合には、この関数が役に立つでしょう (cache_rds() では、キャッシュファイルが削除されたときにキャッシュが無効になりますが、これは引数 rerun = TRUE で実現できます)。

cache_rds() のヘルプページに記載されているように、キャッシュを無効にしたい場合には2つの一般的なケースがあります。

  1. 式の中のコードが変更された場合。例えば、cache_rds({x + 1}) から cache_rds({x + 2}) に変更した場合、キャッシュは自動的に無効になり、式が再評価されます。 ただし、空白やコメントの変更は関係ありませんのでご注意ください。 また、一般的には、解析された式に影響を与えない変更であれば、キャッシュは無効になりません。 たとえば、以下の2つの式は本質的に同じです(したがって、最初の式で cache_rds() を実行していれば、2番目の式はキャッシュを利用することができます)。

    res = xfun::cache_rds({
      Sys.sleep(3  );
      x=1:10;  # semi-colons won't matter
      x+1;
    })
    
    res = xfun::cache_rds({
      Sys.sleep(3)
      x = 1:10  # a comment
      x +
        1  # feel free to make any changes in white spaces
    })
  2. 式の中のグローバル変数の値が変更された場合、例えばyが変更された場合は、キャッシュを無効にして以下の式を再実行したいと思う可能性が高いです。

    res = xfun::cache_rds({
      x = 1:10
      x + y
    })

    これは x が式の中のローカル変数であり、y が外部のグローバル変数(x のようにローカルに作成されない)であるためです。 y が変更されたときにキャッシュを無効にするには、cache_rds()hash 引数で、キャッシュを無効にするかどうかを決めるときに y を考慮する必要があることを知らせます。:

    res = xfun::cache_rds({
      x = 1:10
      x + y
    }, hash = list(y))

    これは、cache_rds() がすべてのグローバル変数を自動的に把握し、その値のリストを hash 引数の値として使用することを求めるものです。

    res = xfun::cache_rds({
      x = 1:10
      x + y
    }, hash = "auto")

パッケージの逆依存性のチェック

R CMD checkknitrrmarkdown の逆依存関係について実行することは、Rパッケージを開発する上で最も嫌いなことです。 なぜなら、その逆依存関係の数は膨大だからです。 関数 rev_check() は、このプロセスにおける私の過去の経験の一部を反映しています。 可能な限り自動化し、(CRAN版と比較して)現在のバージョンのパッケージによってもたらされる可能性のある新しい問題を発見するのを可能な限り簡単にしたつもりです。 最後に、私はただ座ってそれを実行させることができます。

RStudioソースエディタに文字ベクトルを入力する

関数 rstudio_type() は、RStudioのソースエディタに、人間が入力したような文字を入力します。 rstudio::conf 2018の講演準備の際に思いついたものです( 詳細はこの記事 をご覧ください)。

セッション情報の印刷

私は sessionInfo() の出力に完全に満足したことがないので、私のユースケースでより便利になるように微調整しました。 例えば、Rの基本パッケージの名前や、行列生成器/BLAS/LAPACKに関する情報を出力することはほとんどありません。 また、rmarkdownが使用されているときの Pandoc のバージョンのように、セッション情報に追加情報が欲しいこともよくあります。 関数 session_info() は、sessionInfo() の出力を微調整し、他のパッケージが session_info() の出力に情報を追加することを可能にします。

指定したパッケージのバージョンのみを出力するようにすることもできます。 例えば次のようになります。

xfun::session_info(c('xfun', 'rmarkdown', 'knitr', 'tinytex'), dependencies = FALSE)
## R version 3.6.3 (2020-02-29)
## Platform: x86_64-apple-darwin16.7.0 (64-bit)
## Running under: macOS Sierra 10.12.6
## 
## Locale: ja_JP.UTF-8 / ja_JP.UTF-8 / ja_JP.UTF-8 / C / ja_JP.UTF-8 / ja_JP.UTF-8
## 
## Package version:
##   knitr_1.31    rmarkdown_2.7 tinytex_0.31  xfun_0.22    
## 
## Pandoc version: 2.7.3