Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How does did_you_mean gem work?

How does did_you_mean gem work?

Slides for RubyConf Philippines 2015

Yuki Nishijima

March 28, 2015
Tweet

More Decks by Yuki Nishijima

Other Decks in Programming

Transcript

  1. require "did_you_mean" "Yuki".starts_with?("Y") # => NoMethodError: undefined method # `starts_with?’

    for "Yuki":String # # Did you mean? #start_with? # "Yuki".start_with?("Y") # => true
  2. const char *help_unknown_cmd(const char *cmd) { ... /* This abuses

    cmdname->len for levenshtein distance */ for (i = 0, n = 0; i < main_cmds.cnt; i++) { ... main_cmds.names[i]->len = levenshtein(cmd, candidate, 0, 2, 1, 3) + 1; } ... }
  3. const char *help_unknown_cmd(const char *cmd) { ... /* This abuses

    cmdname->len for levenshtein distance */ for (i = 0, n = 0; i < main_cmds.cnt; i++) { ... main_cmds.names[i]->len = levenshtein(cmd, candidate, 0, 2, 1, 3) + 1; } ... }
  4. module Levenshtein def self.distance(str1, str2) str1, str2 = str1.to_s, str2.to_s

    # calculates edit distance... end end str = "Yuki Nishijima" threshold = 2 begin str.starts_with?("Yuki") rescue NoMethodError => error suggestions = str.methods.select do |name| Levenshtein.distance(name, error.name) < threshold end end puts suggestions # => [:start_with?]
  5. module Levenshtein def self.distance(str1, str2) str1, str2 = str1.to_s, str2.to_s

    # calculates edit distance... end end str = "Yuki Nishijima" threshold = 2 begin str.starts_with?("Yuki") rescue NoMethodError => error suggestions = str.methods.select do |name| Levenshtein.distance(name, error.name) < threshold end end puts suggestions # => [:start_with?]
  6. module Levenshtein def self.distance(str1, str2) str1, str2 = str1.to_s, str2.to_s

    # calculates edit distance... end end str = "Yuki Nishijima" threshold = 2 begin str.starts_with?("Yuki") rescue NoMethodError => error suggestions = str.methods.select do |name| Levenshtein.distance(name, error.name) < threshold end end puts suggestions # => [:start_with?]
  7. module Levenshtein def self.distance(str1, str2) str1, str2 = str1.to_s, str2.to_s

    # calculates edit distance... end end str = "Yuki Nishijima" threshold = 2 begin str.starts_with?("Yuki") rescue NoMethodError => error suggestions = str.methods.select do |name| Levenshtein.distance(name, error.name) < threshold end end puts suggestions # => [:start_with?]
  8. module Levenshtein def self.distance(str1, str2) str1, str2 = str1.to_s, str2.to_s

    # calculates edit distance... end end str = "Yuki Nishijima" threshold = 2 begin str.starts_with?("Yuki") rescue NoMethodError => error suggestions = str.methods.select do |name| Levenshtein.distance(name, error.name) < threshold end end puts suggestions # => [:start_with?] [    :<=>,    :==,    ...    :include?,    :start_with?,    :end_with?,    ...    :instance_eval,    :instance_exec,    :__send__,    :__id__   ]
  9. module Levenshtein def self.distance(str1, str2) str1, str2 = str1.to_s, str2.to_s

    # calculates edit distance... end end str = "Yuki Nishijima" threshold = 2 begin str.starts_with?("Yuki") rescue NoMethodError => error suggestions = str.methods.select do |name| Levenshtein.distance(name, error.name) < threshold end end puts suggestions # => [:start_with?] [    :<=>,    :==,    ...    :include?,    :start_with?,    :end_with?,    ...    :instance_eval,    :instance_exec,    :__send__,    :__id__   ] 12   12   11   1   6   10   10   11   10
  10. module Levenshtein def self.distance(str1, str2) str1, str2 = str1.to_s, str2.to_s

    # calculates edit distance... end end str = "Yuki Nishijima" threshold = 2 begin str.starts_with?("Yuki") rescue NoMethodError => error suggestions = str.methods.select do |name| Levenshtein.distance(name, error.name) < threshold end end puts suggestions # => [:start_with?] [    :<=>,    :==,    ...    :include?,    :start_with?,    :end_with?,    ...    :instance_eval,    :instance_exec,    :__send__,    :__id__   ] 12   12   11   1   6   10   10   11   10
  11. module Levenshtein def self.distance(str1, str2) str1, str2 = str1.to_s, str2.to_s

    # calculates edit distance... end end str = "Yuki Nishijima" threshold = 2 begin str.starts_with?("Yuki") rescue NoMethodError => error suggestions = str.methods.select do |name| Levenshtein.distance(name, error.name) < threshold end end puts suggestions # => [:start_with?]
  12. error = (boom rescue $!) puts error.to_s # => undefined

    local variable or method `boom' for main:Object puts error.message # => undefined local variable or method `boom' for main:Object
  13. Exception#to_s Returns exception’s message (or the name of the exception

    if no message is set). http://ruby-doc.org/core-2.2.1/Exception.html#method-i-to_s Exception#message Returns the result of invoking exception.to_s. Normally this returns the exception’s message or name. http://ruby-doc.org/core-2.2.1/Exception.html#method-i-message
  14. class NoMethodError def to_s msg = super msg << "\n\n"

    msg << " Did you mean? #{suggestions.join(", ")}\n" rescue super end end
  15. This needs to be implemented class NoMethodError def to_s msg

    = super msg << "\n\n" msg << " Did you mean? #{suggestions.join(", ")}\n" rescue super end end
  16. module Levenshtein def self.distance(str1, str2) str1, str2 = str1.to_s, str2.to_s

    # calculates edit distance... end end str = "Yuki Nishijima" threshold = 2 begin str.starts_with?("Yuki") rescue NoMethodError => error suggestions = str.methods.select do |name| Levenshtein.distance(name, error.name) < threshold end end puts suggestions # => [:start_with?]
  17. class NoMethodError THRESHOLD = 2 ... def suggestions receiver.methods.select do

    |name| Levenshtein.distance(name, self.name) < THRESHOLD end end end
  18. class NoMethodError THRESHOLD = 2 ... def suggestions receiver.methods.select do

    |name| Levenshtein.distance(name, self.name) < THRESHOLD end end end NoMethodError objects don’t have access to the receiver
  19. error = (boom rescue $!) puts error.to_s # => undefined

    local variable or method `boom' for main:Object puts error.message # => undefined local variable or method `boom' for main:Object
  20. error = (boom rescue $!) puts error.to_s # => undefined

    local variable or method `boom' for main:Object puts error.message # => undefined local variable or method `boom' for main:Object Isn’t that the receiver object?
  21. static VALUE make_no_method_exception(VALUE exc, const char *format, VALUE obj, int

    argc, const VALUE *argv) { int n = 0; VALUE mesg; VALUE args[3]; if (!format) { format = "undefined method `%s' for %s"; } mesg = rb_const_get(exc, rb_intern("message")); if (rb_method_basic_definition_p(CLASS_OF(mesg), '!')) { args[n++] = rb_name_err_mesg_new(mesg, rb_str_new2(format), obj, argv[0]); } else { args[n++] = rb_funcall(mesg, '!', 3, rb_str_new2(format), obj, argv[0]); } args[n++] = argv[0]; if (exc == rb_eNoMethodError) { args[n++] = rb_ary_new4(argc - 1, argv + 1); } return rb_class_new_instance(n, args, exc); }
  22. static VALUE make_no_method_exception(VALUE exc, const char *format, VALUE obj, int

    argc, const VALUE *argv) { int n = 0; VALUE mesg; VALUE args[3]; if (!format) { format = "undefined method `%s' for %s"; } mesg = rb_const_get(exc, rb_intern("message")); if (rb_method_basic_definition_p(CLASS_OF(mesg), '!')) { args[n++] = rb_name_err_mesg_new(mesg, rb_str_new2(format), obj, argv[0]); } else { args[n++] = rb_funcall(mesg, '!', 3, rb_str_new2(format), obj, argv[0]); } args[n++] = argv[0]; if (exc == rb_eNoMethodError) { args[n++] = rb_ary_new4(argc - 1, argv + 1); } return rb_class_new_instance(n, args, exc); } NameError or NoMethodError
  23. static VALUE make_no_method_exception(VALUE exc, const char *format, VALUE obj, int

    argc, const VALUE *argv) { int n = 0; VALUE mesg; VALUE args[3]; if (!format) { format = "undefined method `%s' for %s"; } mesg = rb_const_get(exc, rb_intern("message")); if (rb_method_basic_definition_p(CLASS_OF(mesg), '!')) { args[n++] = rb_name_err_mesg_new(mesg, rb_str_new2(format), obj, argv[0]); } else { args[n++] = rb_funcall(mesg, '!', 3, rb_str_new2(format), obj, argv[0]); } args[n++] = argv[0]; if (exc == rb_eNoMethodError) { args[n++] = rb_ary_new4(argc - 1, argv + 1); } return rb_class_new_instance(n, args, exc); } NameError or NoMethodError
  24. static VALUE make_no_method_exception(VALUE exc, const char *format, VALUE obj, int

    argc, const VALUE *argv) { int n = 0; VALUE mesg; VALUE args[3]; if (!format) { format = "undefined method `%s' for %s"; } mesg = rb_const_get(exc, rb_intern("message")); if (rb_method_basic_definition_p(CLASS_OF(mesg), '!')) { args[n++] = rb_name_err_mesg_new(mesg, rb_str_new2(format), obj, argv[0]); } else { args[n++] = rb_funcall(mesg, '!', 3, rb_str_new2(format), obj, argv[0]); } args[n++] = argv[0]; if (exc == rb_eNoMethodError) { args[n++] = rb_ary_new4(argc - 1, argv + 1); } return rb_class_new_instance(n, args, exc); } NameError or NoMethodError This is the receiver object!
  25. } return rb_class_new_instance(n, args, exc); } static VALUE make_no_method_exception(VALUE exc,

    const char *format, VALUE obj, int argc, const VALUE *argv) { int n = 0; VALUE mesg; VALUE args[3]; if (!format) { format = "undefined method `%s' for %s"; } mesg = rb_const_get(exc, rb_intern("message")); if (rb_method_basic_definition_p(CLASS_OF(mesg), '!')) { args[n++] = rb_name_err_mesg_new(mesg, rb_str_new2(format), obj, argv[0]); } else { args[n++] = rb_funcall(mesg, '!', 3, rb_str_new2(format), obj, argv[0]); } args[n++] = argv[0]; if (exc == rb_eNoMethodError) { args[n++] = rb_ary_new4(argc - 1, argv + 1); NameError or NoMethodError // array[0] = obj # in Ruby rb_ary_store(args[n - 1], 0, obj); class NoMethodError def receiver args.first end end
  26. Initial C extension • Really hacky and unstable • Ruby

    headers required, head to maintain • Doesn’t compile on all Windows machines • Sometimes doesn’t compile even on OS X • Breaks the default behaviour of NoMethodError#args • Breaks other behaviours of Ruby 2.1.x on OS X, other gems (e.g. letter_opener, arel) have been affected
  27. NoMethodError objects internally have access to the receiver object. So

    it shouldn’t be so hard to implement in C. Do you think it’s possible to implement NoMethodError#receiver? @_ko1 @n0kada
  28. #include <ruby.h> static const rb_data_type_t *type; static VALUE name_err_receiver(VALUE self)

    { VALUE *ptr, mesg = rb_attr_get(self, rb_intern("mesg")); TypedData_Get_Struct(mesg, VALUE, type, ptr); return ptr[1]; } void Init_method_receiver() { VALUE err_mesg = rb_funcall(rb_cNameErrorMesg, '!', 3, Qnil, Qnil, Qnil); type = RTYPEDDATA(err_mesg)->type; rb_define_method(rb_eNameError, "receiver", name_err_receiver, 0); }
  29. Better C extension • Simple and stable • Ruby headers

    not required, easy to maintain • Compiles on most platforms • Doesn’t affect existing behaviours
  30. # ext/did_you_mean/extconf.rb require 'mkmf' create_makefile 'did_you_mean/method_receiver' # Rakefile require 'rake/extensiontask'

    Rake::ExtensionTask.new('did_you_mean') do |ext| ext.name = "method_receiver" ext.lib_dir = "lib/did_you_mean" end # From terminal $ rake compile require ‘did_you_mean/method_receiver’ # => actives the C extension
  31. require 'did_you_mean/method_receiver' require 'did_you_mean/levenshtein' class NoMethodError THRESHOLD = 2 def

    to_s msg << super msg << "\n\n" msg << " Did you mean? ##{suggestions.join(', #')}\n" rescue super end def suggestions receiver.methods.select do |name| DidYouMean::Levenshtein.distance(name, self.name) < THRESHOLD end end end "Yuki".starts_with?("Y") # => NoMethodError: undefined method `starts_with?' for "Yuki":String # # Did you mean? #start_with?
  32. user = User.new("Yuki Nishijima") begin suer rescue NameError => error

    suggestions = (local_variables + methods + private_methods).select do |name| Levenshtein.distance(name, error.name) < 2 end end puts suggestions # => [:user]
  33. user = User.new("Yuki Nishijima") begin suer rescue NameError => error

    suggestions = (local_variables + methods + private_methods).select do |name| Levenshtein.distance(name, error.name) < 2 end end puts suggestions # => [:user] This is the only difference. We can re-use most of the code from NoMethodError
  34. require 'did_you_mean/levenshtein' class NameError THRESHOLD = 2 def to_s msg

    << super msg << "\n\n" msg << " Did you mean? ##{suggestions.join(', #')}\n" rescue super end def suggestions names.select do |name| DidYouMean::Levenshtein.distance(name, self.name) < THRESHOLD end end def names local_variables + methods + private_methods end end
  35. require 'did_you_mean/levenshtein' class NameError THRESHOLD = 2 def to_s msg

    << super msg << "\n\n" msg << " Did you mean? ##{suggestions.join(', #')}\n" rescue super end def suggestions names.select do |name| DidYouMean::Levenshtein.distance(name, self.name) < THRESHOLD end end def names local_variables + methods + private_methods end end Same as NoMethodError
  36. require 'did_you_mean/levenshtein' class NameError THRESHOLD = 2 def to_s msg

    << super msg << "\n\n" msg << " Did you mean? ##{suggestions.join(', #')}\n" rescue super end def suggestions names.select do |name| DidYouMean::Levenshtein.distance(name, self.name) < THRESHOLD end end def names local_variables + methods + private_methods end end
  37. require 'did_you_mean/levenshtein' class NameError THRESHOLD = 2 def to_s msg

    << super msg << "\n\n" msg << " Did you mean? ##{suggestions.join(', #')}\n" rescue super end def suggestions names.select do |name| DidYouMean::Levenshtein.distance(name, self.name) < THRESHOLD end end def names local_variables + methods + private_methods end end They don’t return what is expected because the scope is different.
  38. user = User.new("Yuki Nishijima") begin suer rescue NameError => error

    suggestions = (local_variables + methods + private_methods).select do |name| Levenshtein.distance(name, error.name) < 2 end end puts suggestions # => [:user]
  39. user = User.new("Yuki Nishijima") begin suer rescue NameError => error

    suggestions = (local_variables + methods + private_methods).select do |name| Levenshtein.distance(name, error.name) < 2 end end puts suggestions # => [:user]
  40. def create_user user = User.new("Yuki Nishijima") # do something... suer

    end begin create_user rescue NameError => error suggestions = ??????.select do |name| Levenshtein.distance(name, error.name) < 2 end end puts suggestions # => [:user]
  41. def create_user user = User.new("Yuki Nishijima") # do something... suer

    end begin create_user rescue NameError => error suggestions = ??????.select do |name| Levenshtein.distance(name, error.name) < 2 end end puts suggestions # => [:user] 1st scope
  42. def create_user user = User.new("Yuki Nishijima") # do something... suer

    end begin create_user rescue NameError => error suggestions = ??????.select do |name| Levenshtein.distance(name, error.name) < 2 end end puts suggestions # => [:user] 1st scope 2nd scope
  43. A binding is a closure without a function— that is,

    it’s just the referencing environment. Think of bindings as a pointer to a YARV stack frame. binding — Pat Shaughnessy, Ruby Under a Microscope
  44. def create_user user = User.new("Yuki Nishijima") # do something... suer

    end begin create_user rescue NameError => error suggestions = ??????.select do |name| Levenshtein.distance(name, error.name) < 2 end end puts suggestions # => [:user] 1st scope 2nd scope
  45. def create_user user = User.new("Yuki Nishijima") # do something... suer

    end begin create_user rescue NameError => error suggestions = ??????.select do |name| Levenshtein.distance(name, error.name) < 2 end end puts suggestions # => [:user] 1st scope 2nd scope binding binding
  46. interception gem • allows to define hooks that gets called

    whenever an exception is raised • Provides access to both the exception object, and the binding from which it was raised • Works on MRI 1.9.3 - 2.2.1, JRuby, and Rubinius
  47. require 'interception' def create_user user = User.new("Yuki Nishijima") # do

    something... suer end Interception.listen do |exception, binding| exception.instance_variable_set(:@frame_binding, binding) end begin create_user rescue NameError => error bin = error.instance_variable_get(:@frame_binding) names = bin.eval("local_variables + methods + private_methods") suggestions = names.select do |name| Levenshtein.distance(name, error.name) < 2 end end puts suggestions # => [:user]
  48. require 'interception' def create_user user = User.new("Yuki Nishijima") # do

    something... suer end Interception.listen do |exception, binding| exception.instance_variable_set(:@frame_binding, binding) end begin create_user rescue NameError => error bin = error.instance_variable_get(:@frame_binding) names = bin.eval("local_variables + methods + private_methods") suggestions = names.select do |name| Levenshtein.distance(name, error.name) < 2 end end puts suggestions # => [:user]
  49. require 'interception' def create_user user = User.new("Yuki Nishijima") # do

    something... suer end Interception.listen do |exception, binding| exception.instance_variable_set(:@frame_binding, binding) end begin create_user rescue NameError => error bin = error.instance_variable_get(:@frame_binding) names = bin.eval("local_variables + methods + private_methods") suggestions = names.select do |name| Levenshtein.distance(name, error.name) < 2 end end puts suggestions # => [:user] NameError,  self.binding
  50. require 'interception' def create_user user = User.new("Yuki Nishijima") # do

    something... suer end Interception.listen do |exception, binding| exception.instance_variable_set(:@frame_binding, binding) end begin create_user rescue NameError => error bin = error.instance_variable_get(:@frame_binding) names = bin.eval("local_variables + methods + private_methods") suggestions = names.select do |name| Levenshtein.distance(name, error.name) < 2 end end puts suggestions # => [:user]
  51. require 'did_you_mean/levenshtein' require 'interception' Interception.listen do |exception, binding| exception.instance_variable_set(:@frame_binding, binding)

    end class NameError THRESHOLD = 2 def to_s msg << super msg << "\n\n" msg << " Did you mean? #{suggestions.join(', ')}\n" rescue super end def suggestions names.select do |name| DidYouMean::Levenshtein.distance(name, self.name) < THRESHOLD end end def names @frame_binding.eval("local_variables + methods + private_methods") end end
  52. user = "Yuki Nishijima" suer # => NameError: undefined local

    variable or method `suer' for ... # # Did you mean? user
  53. class User < ActiveRecord::Base end USer # => NameError: uninitialized

    constant USer # # Did you mean? User # User.new(nmee: "wrong flrst name") # => ActiveRecord::UnknownAttributeError: unknown attribute: nmee # # Did you mean? name: string #