In Progress
Unit 1, Lesson 21
In Progress

Method Search

Video transcript & code

The other day I got curious about a method name that I'd seen used in various Ruby core source files. The method is called #try_convert, and when sent to the String class with an argument, returns strings for some objects, and nil for others.

String.try_convert("foo")       # => "foo"
String.try_convert(:bar)        # => nil

I found myself wondering if this try_convert message was a common idiom in Ruby core libraries. I felt like I'd seen in other places, but I wasn't sure.

So, how might we go about finding every implementation of methods named try_convert? Is there some way to do it without grepping through C and Ruby source code files?

Well, first we need to know how to determine if a given class implements this method. Ruby has an introspection method for this purpose: method_defined?

String.method_defined?(:try_convert)
# => false

But wait a sec. This returns false. That's not what we expected.

The catch here is that when we ask a class if it defines a given method, it looks through its instance methods.

So, for instance, strings have an instance method called upcase.

"chunky bacon".upcase                    # => "CHUNKY BACON"

And if we ask the string class if it defines the upcase method, it says that yes it does.

String.method_defined?(:upcase) # => true

But try_convert is a class method, otherwise known as a singleton method. So we have to ask the String class for its singleton class, and then ask that if it defines the try_convert method.

String.singleton_class.method_defined?(:try_convert)
# => true

Now we see the answer we were looking for.

And just to make sure we're doing our science properly, let's do a control test to verify that this test returns false for classes which don't define try_convert.

Object.singleton_class.method_defined?(:try_convert)
# => false

OK, now we have a reliable test to determine if a class implements this method. How can we apply this test to all of the classes that Ruby knows about?

For that, we need to use ObjectSpace.

ObjectSpace

ObjectSpace is a special Ruby class that provides a window into the world of live objects inside a running Ruby process.

We can use ObjectSpace to get a list of all of the objects of a given class.

For instance, we can look up all of the IO objects currently active in this program.

ObjectSpace.each_object(IO).to_a
# => [#<File:/home/avdi/.rubies/ruby-2.3.1/lib/ruby/2.3.0/prettyprint.rb (closed)>,
#     #<IO:<STDERR>>,
#     #<IO:<STDOUT>>,
#     #<IO:<STDIN>>,
#     #<IO:fd 9>,
#     #<File:xmptmp-in15439Uje.rb (closed)>]

We can see that the usual standard error, standard output, and standard input objects exist, along with a few other odds and ends.

OK, so this is great for finding objects. But we're interested in listing classes, not objects. Right?

This is true. But fortunately, this is Ruby, and classes are just another kind of object. They are objects of class… Class.

String.class                    # => Class

So to find every class that Ruby currently knows about, all we have to do is look up every object of the type Class.

ObjectSpace.each_object(Class).to_a
# => [TracePoint,
#     Complex::compatible,
#     Complex,
#     Rational::compatible,
#     Rational,
#     FiberError,
#     Fiber,
#     #<Class:Process::Sys>,
#     #<Class:Process::GID>,
#     #<Class:Process::UID>,
#     Process::Tms,
#     Process::Status,
#     Process::Waiter,
#     #<Class:Process>,
#     Thread::ConditionVariable,
#     Thread::SizedQueue,
#     ClosedQueueError,
#     Thread::Queue,
#     Thread::Mutex,
#     ThreadError,
#     ThreadGroup,
#     RubyVM::InstructionSequence,
#     Thread::Backtrace::Location,
#     Thread::Backtrace,
#     #<Class:Thread>,
#     Thread,
#     RubyVM::Env,
#     RubyVM,
#     Enumerator::Yielder,
#     Enumerator::Generator,
#     StopIteration,
#     Enumerator::Lazy,
#     Enumerator,
#     ObjectSpace::WeakMap,
#     #<Class:ObjectSpace>,
#     #<Class:GC::Profiler>,
#     #<Class:GC>,
#     #<Class:Math>,
#     Math::DomainError,
#     Binding,
#     UnboundMethod,
#     Method,
#     SystemStackError,
#     LocalJumpError,
#     Proc,
#     #<Class:Signal>,
#     Random,
#     #<Class:Time>,
#     Time,
#     Dir,
#     File::Stat,
#     #<Class:RbConfig>,
#     #<Class:FileTest>,
#     File,
#     ARGF.class,
#     IO::EINPROGRESSWaitWritable,
#     IO::EINPROGRESSWaitReadable,
#     IO::EAGAINWaitWritable,
#     IO::EAGAINWaitReadable,
#     IO,
#     EOFError,
#     IOError,
#     Range,
#     #<Class:Marshal>,
#     Encoding::Converter,
#     Encoding::ConverterNotFoundError,
#     Encoding::InvalidByteSequenceError,
#     Encoding::UndefinedConversionError,
#     MatchData,
#     Regexp,
#     RegexpError,
#     Struct,
#     #<Class:#<Object:0x0055811d7710e8>>,
#     Hash,
#     Array,
#     Errno::EHWPOISON,
#     Errno::ERFKILL,
#     Errno::EOWNERDEAD,
#     Errno::ENOTRECOVERABLE,
#     Errno::ENOMEDIUM,
#     Errno::ENOKEY,
#     Errno::EMEDIUMTYPE,
#     Errno::EKEYREVOKED,
#     Errno::EKEYREJECTED,
#     Errno::EKEYEXPIRED,
#     Errno::ECANCELED,
#     Errno::EDQUOT,
#     Errno::EREMOTEIO,
#     Errno::EISNAM,
#     Errno::ENAVAIL,
#     Errno::ENOTNAM,
#     Errno::EUCLEAN,
#     Errno::ESTALE,
#     Errno::EINPROGRESS,
#     Errno::EALREADY,
#     Errno::EHOSTUNREACH,
#     Errno::EHOSTDOWN,
#     Errno::ECONNREFUSED,
#     Errno::ETIMEDOUT,
#     Errno::ETOOMANYREFS,
#     Errno::ESHUTDOWN,
#     Errno::ENOTCONN,
#     Errno::EISCONN,
#     Errno::ENOBUFS,
#     Errno::ECONNRESET,
#     Errno::ECONNABORTED,
#     Errno::ENETRESET,
#     Errno::ENETUNREACH,
#     Errno::ENETDOWN,
#     Errno::EADDRNOTAVAIL,
#     Errno::EADDRINUSE,
#     Errno::EAFNOSUPPORT,
#     Errno::EPFNOSUPPORT,
#     Errno::EOPNOTSUPP,
#     Errno::ESOCKTNOSUPPORT,
#     Errno::EPROTONOSUPPORT,
#     Errno::ENOPROTOOPT,
#     Errno::EPROTOTYPE,
#     Errno::EMSGSIZE,
#     Errno::EDESTADDRREQ,
#     Errno::ENOTSOCK,
#     Errno::EUSERS,
#     Errno::ESTRPIPE,
#     Errno::ERESTART,
#     Errno::EILSEQ,
#     Errno::ELIBEXEC,
#     Errno::ELIBMAX,
#     Errno::ELIBSCN,
#     Errno::ELIBBAD,
#     Errno::ELIBACC,
#     Errno::EREMCHG,
#     Errno::EBADFD,
#     Errno::ENOTUNIQ,
#     Errno::EOVERFLOW,
#     Errno::EBADMSG,
#     Errno::EDOTDOT,
#     Errno::EMULTIHOP,
#     Errno::EPROTO,
#     Errno::ECOMM,
#     Errno::ESRMNT,
#     Errno::EADV,
#     Errno::ENOLINK,
#     Errno::EREMOTE,
#     Errno::ENOPKG,
#     Errno::ENONET,
#     Errno::ENOSR,
#     Errno::ETIME,
#     Errno::ENODATA,
#     Errno::ENOSTR,
#     Errno::EBFONT,
#     Errno::EBADSLT,
#     Errno::EBADRQC,
#     Errno::ENOANO,
#     Errno::EXFULL,
#     Errno::EBADR,
#     Errno::EBADE,
#     Errno::EL2HLT,
#     Errno::ENOCSI,
#     Errno::EUNATCH,
#     Errno::ELNRNG,
#     Errno::EL3RST,
#     Errno::EL3HLT,
#     Errno::EL2NSYNC,
#     Errno::ECHRNG,
#     Errno::EIDRM,
#     Errno::ENOMSG,
#     Errno::ELOOP,
#     Errno::ENOTEMPTY,
#     Errno::ENOSYS,
#     Errno::ENOLCK,
#     Errno::ENAMETOOLONG,
#     Errno::EDEADLK,
#     Errno::ERANGE,
#     Errno::EDOM,
#     Errno::EPIPE,
#     Errno::EMLINK,
#     Errno::EROFS,
#     Errno::ESPIPE,
#     Errno::ENOSPC,
#     Errno::EFBIG,
#     Errno::ETXTBSY,
#     Errno::ENOTTY,
#     Errno::EMFILE,
#     Errno::ENFILE,
#     Errno::EINVAL,
#     Errno::EISDIR,
#     Errno::ENOTDIR,
#     Errno::ENODEV,
#     Errno::EXDEV,
#     Errno::EEXIST,
#     Errno::EBUSY,
#     Errno::ENOTBLK,
#     Errno::EFAULT,
#     Errno::EACCES,
#     Errno::ENOMEM,
#     Errno::EAGAIN,
#     Errno::ECHILD,
#     Errno::EBADF,
#     Errno::ENOEXEC,
#     Errno::E2BIG,
#     Errno::ENXIO,
#     Errno::EIO,
#     Errno::EINTR,
#     Errno::ESRCH,
#     Errno::ENOENT,
#     Errno::EPERM,
#     Errno::NOERROR,
#     Bignum,
#     Float,
#     Fixnum,
#     Integer,
#     Numeric,
#     FloatDomainError,
#     ZeroDivisionError,
#     UncaughtThrowError,
#     SystemCallError,
#     Encoding::CompatibilityError,
#     EncodingError,
#     NoMemoryError,
#     SecurityError,
#     RuntimeError,
#     NoMethodError,
#     NameError::message,
#     NameError,
#     NotImplementedError,
#     LoadError,
#     SyntaxError,
#     ScriptError,
#     RangeError,
#     KeyError,
#     IndexError,
#     ArgumentError,
#     TypeError,
#     StandardError,
#     Interrupt,
#     SignalException,
#     fatal,
#     SystemExit,
#     Exception,
#     Symbol,
#     String,
#     Encoding,
#     #<Class:#<Object:0x0055811d786650>>,
#     FalseClass,
#     TrueClass,
#     Data,
#     #<Class:BasicObject>,
#     #<Class:Object>,
#     #<Class:Module>,
#     #<Class:Class>,
#     NilClass,
#     #<Class:Kernel>,
#     Class,
#     Module,
#     Object,
#     BasicObject,
#     #<Class:Gem>,
#     #<Class:SeeingIsBelieving::EventStream::Producer::NullQueue>,
#     SeeingIsBelieving::EventStream::Producer,
#     #<Class:SeeingIsBelieving::EventStream::Events::Finished>,
#     SeeingIsBelieving::EventStream::Events::Finished,
#     #<Class:SeeingIsBelieving::EventStream::Events::EventStreamClosed>,
#     SeeingIsBelieving::EventStream::Events::EventStreamClosed,
#     #<Class:SeeingIsBelieving::EventStream::Events::StderrClosed>,
#     SeeingIsBelieving::EventStream::Events::StderrClosed,
#     #<Class:SeeingIsBelieving::EventStream::Events::StdoutClosed>,
#     SeeingIsBelieving::EventStream::Events::StdoutClosed,
#     #<Class:SeeingIsBelieving::EventStream::Events::ResultsTruncated>,
#     SeeingIsBelieving::EventStream::Events::ResultsTruncated,
#     #<Class:Gem::Deprecate>,
#     SeeingIsBelieving::EventStream::Events::Exception,
#     #<Class:SeeingIsBelieving::EventStream::Events::LineResult>,
#     SeeingIsBelieving::EventStream::Events::LineResult,
#     #<Class:SeeingIsBelieving::EventStream::Events::Exec>,
#     SeeingIsBelieving::EventStream::Events::Exec,
#     #<Class:SeeingIsBelieving::EventStream::Events::Timeout>,
#     SeeingIsBelieving::EventStream::Events::Timeout,
#     #<Class:SeeingIsBelieving::EventStream::Events::Exitstatus>,
#     SeeingIsBelieving::EventStream::Events::Exitstatus,
#     #<Class:SeeingIsBelieving::EventStream::Events::Exception>,
#     #<Class:SeeingIsBelieving::EventStream::Events::RubyVersion>,
#     SeeingIsBelieving::EventStream::Events::RubyVersion,
#     #<Class:SeeingIsBelieving::EventStream::Events::SiBVersion>,
#     SeeingIsBelieving::EventStream::Events::SiBVersion,
#     #<Class:SeeingIsBelieving::EventStream::Events::NumLines>,
#     SeeingIsBelieving::EventStream::Events::NumLines,
#     #<Class:SeeingIsBelieving::EventStream::Events::Filename>,
#     SeeingIsBelieving::EventStream::Events::Filename,
#     #<Class:SeeingIsBelieving::EventStream::Events::MaxLineCaptures>,
#     SeeingIsBelieving::EventStream::Events::MaxLineCaptures,
#     #<Class:SeeingIsBelieving::EventStream::Events::Stderr>,
#     SeeingIsBelieving::EventStream::Events::Stderr,
#     #<Class:SeeingIsBelieving::EventStream::Events::Stdout>,
#     SeeingIsBelieving::EventStream::Events::Stdout,
#     #<Class:SeeingIsBelieving::EventStream::Event>,
#     SeeingIsBelieving::EventStream::Event,
#     Gem::SourceFetchProblem,
#     Gem::PlatformMismatch,
#     Gem::ErrorReason,
#     Gem::ConflictError,
#     SeeingIsBelieving::HashStruct::Attr,
#     Gem::LoadError,
#     #<Class:SeeingIsBelieving::HashStruct>,
#     SeeingIsBelieving::HashStruct,
#     SeeingIsBelieving,
#     Gem::PathSupport,
#     #<Class:Gem::Version>,
#     Gem::Version,
#     Monitor,
#     #<Class:MonitorMixin>,
#     MonitorMixin::ConditionVariable,
#     MonitorMixin::ConditionVariable::Timeout,
#     Gem::Requirement::BadRequirementError,
#     #<Class:0x0055811da1cea0>,
#     #<Class:Gem::Requirement>,
#     Gem::Requirement,
#     #<Class:Gem::Platform>,
#     Gem::Platform,
#     #<Class:Gem::BasicSpecification>,
#     Gem::BasicSpecification,
#     #<Class:Gem::List>,
#     Gem::List,
#     Gem::StubSpecification::StubLine,
#     #<Class:Gem::StubSpecification>,
#     Gem::StubSpecification,
#     PP::SingleLine,
#     #<Class:0x0055811da85590>,
#     #<Class:PP>,
#     PP,
#     PrettyPrint::SingleLine,
#     PrettyPrint::GroupQueue,
#     #<Class:Gem::Specification>,
#     Gem::Specification,
#     PrettyPrint::Group,
#     PrettyPrint::Breakable,
#     StringIO,
#     #<Class:PrettyPrint>,
#     PrettyPrint,
#     PrettyPrint::Text,
#     #<Class:#<Object:0x0055811da92f38>>,
#     Gem::UnsatisfiableDependencyError,
#     Gem::SystemExitException,
#     Gem::VerificationError,
#     Gem::RubyVersionMismatch,
#     Gem::RemoteSourceException,
#     Gem::RemoteInstallationSkipped,
#     Gem::RemoteInstallationCancelled,
#     Gem::RemoteError,
#     Gem::OperationNotSupportedError,
#     Gem::InvalidSpecificationException,
#     Gem::InstallError,
#     Gem::ImpossibleDependenciesError,
#     Gem::SpecificGemNotFoundException,
#     Gem::GemNotFoundException,
#     Gem::FormatException,
#     Gem::FilePermissionError,
#     Gem::EndOfYAMLException,
#     Gem::DocumentError,
#     Gem::GemNotInHomeException,
#     Gem::DependencyResolutionError,
#     Gem::DependencyRemovalException,
#     Gem::DependencyError,
#     Gem::CommandLineError,
#     Gem::Exception]

This is a pretty long list, but if we poke through it we run across some familiar faces, like String, Array, and Integer.

It looks like this is indeed the list we want to search through.

So now let's combine everything we've learned so far. We'll narrow down the list of class objects, using our class method implementation test as the filter.

ObjectSpace.each_object(Class).select{|c|
  c.singleton_class.method_defined?(:try_convert)
}
# => [File, IO, Regexp, Hash, Array, String]

It looks like my suspicions were correct: try_convert is implemented by several of Ruby's most fundamental classes; not just by String.

This little excursion into metaprogramming illustrates one of the great strengths of the Ruby language: it is sufficiently introspective that nearly any question we have can be answered "live" by Ruby itself. We just have to know how to pose the question.

So after all that, what is the purpose of this mysterious try_convert method? Well, we can talk about that on another day. Happy hacking!

Responses