R に「もしかして」機能を追加する


対話コマンド環境において,コマンドを打ち間違えた時,本当に打ちたかったコマンドがサジェストされる機能,便利ですよね[A]。 R にも欲しいですね。ということで作りましょう。

以下のような感じに動くものを作ります。

> t_test
 エラー:  オブジェクト 't_test' がありません 
 Did you mean 't.test'?
> map
 エラー:  オブジェクト 'map' がありません 
 Did you mean any of ['mad', 'Map', 'max']?

まずは関数 (または変数) 一覧の取得から始めます。関数一覧は ls 関数により取得することができます。対話環境の基本となる環境である .GlobalEnv から遡っていき,アクセスできる関数一覧を取得します。

getNames <- function(envir=.GlobalEnv){
   current <- envir
   table <- character(0L)
   while (!identical(current, emptyenv())) {
      variables <- ls(envir=current)
      variables <- variables[grep("^[.a-zA-Z][_.a-zA-Z0-9]*$", variables)]
      table <- union(table, variables)
      current <- parent.env(current)
   }
   sort(table)
}

親環境を遡っていくと,最終的に空の環境 (emptyenv()) に到達するので,そこまで parent.env 関数で遡っていきます。環境ごとに ls 関数で変数一覧を取得して溜め込んでいけば, .GlobalEnv からアクセスできる関数一覧が取得できます。単純に ls 関数を使うだけだと, %% のような演算子も引っかかってきますので,正規表現でそれっぽい名前のものだけをフィルタリングしておきます。最後の sort は結果を綺麗にするためです。

次に,入力に近い関数を絞り込みます。文字列間の距離は adist 関数を使えば取得できます。 adist 関数は Levenshtein 距離を計算します。ここでは 2 文字までの誤差を許容して,最も距離が近い関数をフィルタリングする方針で探索してみます[B]

findSimilarName <- function(x, names, threshold=2) {
   d <- adist(x, names)
   names[d == min(d) & d <= threshold]
}

類似した名前の取得はできたので,これをエラー処理時に適切に呼び出します。

エラーメッセージに「オブジェクト 'var' がありません」というメッセージを取得し, var にあたる部分が最終的に欲しい文字列です。

まずはエラーメッセージを取得します。エラーメッセージを取得するには, geterrmessage 関数を用います。エラー後にこの関数を呼ぶと,エラーメッセージが文字列として得られます。エラーが起こったタイミングでこの関数を呼ぶ方法は後述します。

エラーメッセージを取得したら,上記のエラーメッセージを探します。自分の環境で用いるだけなら特に気にしなくても良いのですが, R は多言語対応であるため,英語版でも日本語版でも動くようにしたいところです。 R で多言語対応の処理を行うには ngettextgettext 関数を用います。詳細は割愛しますが,以下の方法で上記のエラーメッセージが取得できます。

gettext("object '%s' not found", domain="R")

これを用いれば,以下のような関数をエラー後に呼べば,エラーになった変数が取得できます。

getMissingVariable <- function() {
   errorMessage <- geterrmessage()
   notFound <- gettext("object '%s' not found", domain="R")
   pattern <- sub("'%s'", "'([^']+)'", sprintf("^.*%s.*$", notFound))
   sub(pattern, "\\1", errorMessage)
}

簡単に説明すると,エラーメッセージを取得し,それに対応する正規表現パターンを作成して必要な部分 (変数名) を取得しています。

さて,以上で道具は揃いました。まずは以下の DYM 関数を定義します。

DYM <- function() {
   missingVariable <- getMissingVariable()
   availableVariables <- getNames()
   names <- findSimilarName(missingVariable, availableVariables)
   if (length(names) > 0L) {
      message <- ngettext(length(names), " Did you mean %s?", " Did you mean any of [%s]?")
      hints <- sapply(names, sprintf, fmt="'%s'")
      message(sprintf(message, paste(hints, collapse=", ")))
   }
}

そしてこれを optionserror に設定します。

options(error = DYM)

これでエラー時に DYM 関数が呼ばれます。エラーメッセージにオブジェクトが存在しないというエラーメッセージが含まれていた場合,変数名を取得し,類似した関数を探索します。もし類似した関数が見つかった場合には,その結果を出力します。

これを普段から利用したい場合は, .Rprofile に上記の関数を定義すれば OK です。ただし,上記では関数をそれぞれ定義しましたが, .Rprofile に書く場合は,余計な関数が見えないように, options(error = function() { ... }) のように, 1 つの無名関数にまとめて記述した方が無難です。既に options(error) 定義が存在する場合は,既存の関数の先頭に DYM の処理を追加すると良いでしょう。

[2015-02-11 23:30] パッケージ化しました。どうやら Rdym という同じことをするパッケージが既にあったらしいです[C]

更新履歴

脚注

  1. 例えば Git にそのような機能が実装されていますね。 []
  2. R には agrep 関数という, grep の fuzzy 版の関数があるので,これを用いても良いでしょう。 []
  3. でも日本語環境でうまくサジェストしてくれなかったり,候補が 1 つしかなかったりするので微妙。 []