did_you_mean 1.1.0 でやってきた8つの大きな変更

[Kaminari 1.0.0 でやってくる5つの大きな変更](http://qiita.com/yuki24/items/aab0d8e417d6fe546688)の次は did_you_mean gem だ。

2017年が始まって既に10日経つが、Rubyist にとって 1月は新しい Ruby のバージョンがリリースされた直後である。仕事で開発しているアプリケーションを新しい Ruby のバージョンへ更新しようという人も多いのではないだろうか。

did_you_mean gem もクリスマス前に新しいバージョン 1.1.0 をリリースしており、Ruby 2.4.0 では新しいバージョンがバンドルされている。そこで、バージョン 1.0.0 からの変更点を振り返ってみようと思う。

実際には、1.0.0 との後に 1.0.1 と 1.0.2 がリリースされているが、これらのバージョンは明示的に `gem update` を実行しない限りインストールされないので、それらの変更も含めて紹介したい。

## バージョン 1.1.0 は Ruby 2.4 でのみ動作

did_you_mean gem は Ruby 本体に追加された新しい機能を積極的に使って、その精度を高めようという方針を取っている。Ruby 2.4 では `NoMethodError#private_call?` というメソッドが新しく追加されており、これが did_you_mean gem のバージョン 1.1.0 で使われているので、それ以前のバージョンでは動作しない。もし Ruby 2.3 をまだ使っている場合は、バージョン 1.0.2 を利用してほしい。


## ユーザーが呼び出した名前をサジェストしないようにした

これまでよく報告されていた問題の一つに、実際に呼ばれたメソッド名がサジェストされてしまうというものがあった。よくある例として次のようなものがある。

Ruby 2.3 and did_you_mean 1.0.0:

```ruby
bar; bar = 1
# => NameError: undefined local variable or method `bar' for ...
#    Did you mean?  bar
```

MRI の仕様上、`Kernel#local_variables` の返り値がまだ初期化されていない変数を含んでいることも気になるのだが、呼ばれた名前と同じ名前がサジェストされるケースは他にも報告されていた。そこで、そのようなサジェストはどのような場合でも表示されないように変更を加えた。

Ruby 2.4 and did_you_mean 1.1.0:

```ruby
bar; bar = 1
# => NameError: undefined local variable or method `bar' for ...
```


## `Struct` から `NameError` が発生した時にサジェストするようになった

`Struct` オブジェクトには `#[]` というメソッドが定義されており、`Hash#[]` と同じように振る舞う。ただし、`Struct` オブジェクトのメンバーにないキーが渡された場合には、`NameError` が発生する（`Hash` は単に `nil` を返す）。バージョン 1.1.0 では、`Struct#[]` から `NameError` が発生した場合も候補をサジェストするようになった。

Ruby 2.3 and did_you_mean 1.0.0:

```ruby
Struct.new(:foo).new[:fooo]
# => NameError: no member 'fooo' in struct
```

Ruby 2.4 and did_you_mean 1.1.0:

```ruby
Struct.new(:foo).new[:fooo]
# => NameError: no member 'fooo' in struct
#    Did you mean?  foo
#                   foo=
```

この変更は @ksss さんによって提案、実装された。ありがとうございました。

## `nil` から `NoMethodError` が発生した時に（あまり）サジェストしないようにした

レシーバーが `nil` であった時に変な候補がサジェストされるという問題は度々報告されていたが、うまい解決方法が思いつかなかった。`nil` から `NoMethodError` が発生した場合、本当に `nil` のメソッドを呼ぼうとしたのにタイポしてしまった場合と、呼びたいメソッド名は正しいが、レシーバーが意図せず `nil` となってしまった場合の2つのケースが考えられる。前者ではサジェストをすべきだが、後者ではすべきではない。しかし、これら2つのケースをうまく判別する方法が思いつかなかった。かといって、遭遇頻度を考えるとこの問題を無視するわけにもいかないので、とりあえずの解決案として、`nil` に元から定義されているメソッドを候補から外すことにした。

Ruby 2.3 and did_you_mean 1.0.0:

```ruby
@users.map {|user| ... }
# => NoMethodError: undefined method `map' for nil:NilClass
#    Did you mean?  tap
```

Ruby 2.4 and did_you_mean 1.1.0:

```ruby
@users.map {|user| ... }
# => NoMethodError: undefined method `map' for nil:NilClass
```

この実装は、とりあえず遭遇頻度の高い問題を解決しているものの、これに満足しているわけではない。訂正されることを期待している状況で何も提示されないという状況が、きっと生まれてしまうだろう。何か良い方法があればぜひ提案してほしい。

## private メソッドの名前を状況に応じて候補として適切に扱うようになった

バージョン 1.0.0 では、状況によって private メソッドの名前を不適切にサジェストしてしまうという既知のバグがあった。たとえば、次のような例を考える。

Ruby 2.3 and did_you_mean 1.0.0:

```ruby
File.raed 'path/to/file.csv'
# => NoMethodError: undefined method `raed' for File:Class
#    Did you mean?  read
#                   rand
```

この場合、public メソッドである `File#read` を呼びたいというのは明らかであるが、 private メソッドで呼ぶことのできない `Kernel#rand` メソッドまでサジェストしてしまっている。これは、Ruby 2.3 では「private メソッドを呼ぶことのできるコールであるか」を判別する方法がなかったので、引数が渡されていたらどのような状況でも private メソッドの名前全てを候補として扱っていた。Ruby 2.4 では、新しく追加された `NoMethodError#private_call?` でそれ判別できるようになったので、private メソッドの名前を適切に扱えるようになった。

Ruby 2.4 and did_you_mean 1.1.0:

```ruby
File.raed 'path/to/file.csv'
# => NoMethodError: undefined method `raed' for File:Class
#    Did you mean?  read
```

## スペルチェッカーを public interface として利用できるようになった

バージョン 1.1.0 より、did_you_mean gem が内部で使用しているスペルチェッカーを簡単に再利用できるようになった。

Ruby 2.4 and did_you_mean 1.1.0:

```ruby
DidYouMean::SpellChecker.new(dictionary: ['email', 'fail', 'eval']).correct('meail')
# => ['email']
```

## Experimental 機能が追加された

実は、1.0.0 よりも前のバージョンではより多くの状況で "Did you mean?" が表示されるようになっていた。しかし、Ruby 2.3 にバンドルするのに合わせて、パフォーマンスに影響の出そうな機能や、Rails 上でのみ有効な機能を削除した。しかし、個人的に気に入ってた機能もあったのにそれが使えなくなってしまうのは惜しい。そこで、削除された機能のうち、Ruby 本体の機能だけで実現可能なものは、experimental 機能として did_you_mean gem に同梱することにした。また、Rails 上でのみ有効な機能は別に [did_you_mean-activerecord gem](https://github.com/yuki24/did_you_mean-activerecord) として公開している。did_you_mean gem のバージョン 1.1.0 に同梱されている experimental 機能を有効にするには、

```ruby
require 'did_you_mean/experimental'
```

を追加するだけでいい。これを追加にすると、
 
* インスタンス変数の名前をタイポしていて NoMethodError が起きた場合:

    ```ruby
    require 'did_you_mean/experimental'
    
    @full_name = "Yuki Nishijima"
    @full_anme.split(" ")
    # => NoMethodError: undefined method `split' for nil:NilClass
    #    Did you mean?  @full_name
    ```

* ハッシュアクセス時のキーにタイポがある場合:

    ```ruby
    require 'did_you_mean/experimental'

    hash = {foo: 1, bar: 2, baz: 3}
    hash.fetch(:fooo)
    # => KeyError: key not found: :fooo
    #    Did you mean?  :foo
    ```

* クラスの `initialize` にタイポがある場合:

    ```ruby
    require 'did_you_mean/experimental'

    class Person
      def intialize
        ...
      end
    end
    # => warning: intialize might be misspelled, perhaps you meant initialize?
    ```

という、3つの experimental 機能が有効になる。

ハッシュアクセス時のサジェストも `Struct` のサジェスト同様、@ksss さんによって提案、実装された。ありがとうございました。


## JRuby に対応した

MRI 2.3.0 に続き、JRuby も 9.1.3.0 から did_you_mean gem が自動でインストールされて require されるようになった。JRuby では、ローカル変数をタイポしても訂正されない、前述の experimental 機能が完全に動かない可能性があるというような違いはあるものの、ほぼ MRI と同等の機能を提供することに成功している。今後も MRI だけでなく、did_you_mean gem が動作する実装を増やしていきたい。


## Feedback をお待ちしています
1.0.0 から1年間で様々な変更があった。そんなにたいした変更はないだろうと思っていたが、書き出してみたらたくさんあった！1年間継続して積み重ねると意外と遠くまで来られるものだ。

もしバグ報告や改善案などがあれば、ぜひ [GitHub issues](https://github.com/yuki24/did_you_mean/issues) までご連絡下さい。特に、スペルチェッカーの精度は真面目に研究を行えばまだかなり改善できるはずなので、高専、大学、大学院で研究したいという方も募集しています（そんな物好きはいないと思うかもしれないけど...）。