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

  • 37
    いいね
  • 0
    コメント

Kaminari 1.0.0 でやってくる5つの大きな変更の次は 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:

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:

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:

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

Ruby 2.4 and did_you_mean 1.1.0:

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:

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

Ruby 2.4 and did_you_mean 1.1.0:

@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:

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:

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:

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 として公開している。did_you_mean gem のバージョン 1.1.0 に同梱されている experimental 機能を有効にするには、

require 'did_you_mean/experimental'

を追加するだけでいい。これを追加にすると、

  • インスタンス変数の名前をタイポしていて NoMethodError が起きた場合:

    require 'did_you_mean/experimental'
    
    @full_name = "Yuki Nishijima"
    @full_anme.split(" ")
    # => NoMethodError: undefined method `split' for nil:NilClass
    #    Did you mean?  @full_name
    
  • ハッシュアクセス時のキーにタイポがある場合:

    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 にタイポがある場合:

    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 までご連絡下さい。特に、スペルチェッカーの精度は真面目に研究を行えばまだかなり改善できるはずなので、高専、大学、大学院で研究したいという方も募集しています(そんな物好きはいないと思うかもしれないけど...)。