Ruby为什么能识别类外的方法,但不能识别类内的方法?

huangapple go评论63阅读模式
英文:

Why does ruby recognise a method outside of a class, but not inside?

问题

我正在尝试构建一个简单的语言翻译程序。我导入了'language_converter'宝石来帮助实现这个目标。我编写了以下代码:

require 'language_converter'

class Translator
  def initialize
    @to = 'ja';
    @from = 'en';
  end

  def translate text
    lc(text, @to, @from)
  end
end

#puts lc('welcome to Japan!', 'ja','en');

t = Translator.new

p t.translate('welcome to Japan!');
这段代码导致错误:`undefined method 'lc' for #<Translator:0x0000000101167a90 @to="ja", @from="en"> (NoMethodError)`
然而,当我取消注释第15行的代码时,Ruby可以访问lc方法并返回一些日文。有人知道为什么该方法在类内部没有被'定义'吗?

编辑:[language-converter][1]宝石不是我的。此外,我在其主页上找不到源代码。

我还尝试在`lc`方法之前添加两个冒号,如下所示:`::lc(text, @to, @from)`。这导致错误:`语法错误,意外的局部变量或方法,期望常量`
英文:

I am trying to build a simple language translating program. I imported the 'language_converter' gem to aid with this goal. I wrote the following code:

require &#39;language_converter&#39;

class Translator
	def initialize
		@to = &#39;ja&#39;;  
		@from = &#39;en&#39;; 	
	end

	def translate text
		lc(text, @to,@from)
	end
end

#puts lc(&#39;welcome to Japan!&#39;, &#39;ja&#39;,&#39;en&#39;);
 
t = Translator.new

p t.translate(&#39;welcome to Japan!&#39;);

This code results in the error: undefined method &#39;lc&#39; for #&lt;Translator:0x0000000101167a90 @to=&quot;ja&quot;, @from=&quot;en&quot;&gt; (NoMethodError)
However, when i uncomment the code on line 15, ruby can access the lc method and return some japanese. Does anyone know why the method is 'defined' outside of the class but not inside?

Edit: the language-converter gem is not my own. also, I cannot find the source code on its homepage.

I have also tried adding two semicolons before the lc method like so: ::lc(text, @to,@from). This results in the error: syntax error, unexpected local variable or method, expecting constant

答案1

得分: 4

这个 gem 已经有超过 10 年的历史,只有一个方法,并且该方法实现为一个类方法。

最好的做法是在你的应用程序中使用现代的 Ruby 语法和正确的错误处理来重写该方法。

关于这个 gem 中 lib/language_converter.rb 的代码如下:

require 'net/http'
require 'rubygems'
require "uri"
require 'json'

class UnSupportedLanguage < RuntimeError

  def initialize(message='')
    @msg = "not supported."
  end
end

def self.lc( text, to, from='en' )

  begin

    uri = URI.parse("http://mymemory.translated.net/api/get")

    response = Net::HTTP.post_form(uri, {"q" => text,"langpair"=>"#{from.to_s.downcase}|#{to.to_s.downcase}", "per_page" => "50"})

    json_response_body = JSON.parse( response.body )

    if json_response_body['responseStatus'] == 200
      json_response_body['responseData']['translatedText']
    else
      puts json_response_body['responseDetails']
      raise StandardError, response['responseDetails']
    end
  rescue UnSupportedLanguage
    raise UnSupportedLanguage.new
  rescue => err_msg
    puts "#{err_msg}"
  end

end

希望这有助于你理解代码。

英文:

The gem is more than 10 years old and only has one method. And that method is implemented as a class method.

You are properly better off with just rewriting that method in your application with a modern Ruby syntax and proper error handling.

For reference, this it how lib/language_converter.rb in the gem looks like:

require &#39;net/http&#39;
require &#39;rubygems&#39;
require &quot;uri&quot;
require &#39;json&#39;

class UnSupportedLanguage &lt; RuntimeError

  def initialize(message=&#39;&#39;)
    @msg = &quot;not supported.&quot;
  end
end


  def self.lc( text, to, from=&#39;en&#39; )

    begin

      uri = URI.parse(&quot;http://mymemory.translated.net/api/get&quot;)

      response = Net::HTTP.post_form(uri, {&quot;q&quot; =&gt; text,&quot;langpair&quot;=&gt;&quot;#{from.to_s.downcase}|#{to.to_s.downcase}&quot;, &quot;per_page&quot; =&gt; &quot;50&quot;})

      json_response_body = JSON.parse( response.body )

      if json_response_body[&#39;responseStatus&#39;] == 200
        json_response_body[&#39;responseData&#39;][&#39;translatedText&#39;]
      else
        puts json_response_body[&#39;responseDetails&#39;]
        raise StandardError, response[&#39;responseDetails&#39;]
      end
    rescue UnSupportedLanguage
      raise UnSupportedLanguage.new
    rescue =&gt; err_msg
      puts &quot;#{err_msg}&quot;
    end

  end

答案2

得分: 0

Does anyone know why the method is 'defined' outside of the class but not inside?
有人知道为什么方法在类外部被“定义”,而不在类内部吗?

The method lookup algorithm in Ruby is relatively simple.
Ruby中的方法查找算法相对简单。

  1. Retrieve the value of the hidden class pointer of the receiver.

  2. 检索接收器的隐藏class指针的值。

  3. Check whether the method table contains the method.

  4. 检查方法表是否包含该方法。

    1. If yes, you are done.

    2. 如果是,就完成了。

    3. If not, continue to step #3.

    4. 如果不是,继续到步骤#3。

  5. Retrieve the value of the superclass pointer.

  6. 检索超类指针的值。

    1. If there is no superclass, go to step #4.

    2. 如果没有超类,转到步骤#4。

    3. If there is a superclass, repeat from step #2.

    4. 如果有超类,从步骤#2重复。

  7. Start the algorithm from the beginning with the message selector method_missing, passing the name of the original message and its arguments as arguments.

  8. 从头开始使用消息选择器method_missing启动算法,将原始消息的名称和其参数作为参数传递。

And that's it. This is always guaranteed to terminate because the top-level class always has a method named method_missing.
就是这样。这总是保证会终止,因为顶级类始终具有名为method_missing的方法。

Now, you might ask: but wait, what about singleton classes?
现在,您可能会问:但等等,单例类怎么办?

The answer is: the hidden class pointer of an object points to its singleton class, and the singleton class's superclass pointer points (directly or indirectly) to the "actual" class.
答案是:对象的隐藏class指针指向其单例类,而单例类的超类指针指向(直接或间接)“实际”类。

Now, you might ask: but wait, why doesn't Object#class return the singleton class, then?
现在,您可能会问:但等等,为什么Object#class不返回单例类呢?

The answer is: Object#class doesn't directly return the hidden class pointer. Instead, it follows the superclass chain until it finds the first "normal" class.
答案是:Object#class不直接返回隐藏的class指针。相反,它会遵循超类链,直到找到第一个“正常”的类。

Now, you might ask: but wait, what about mixins using Module#include?
现在,您可能会问:但等等,使用Module#include的混合有什么问题?

The answer is: Module#include (or rather Module#append_features) synthesizes a new class (sometimes called an include class) which shares its method table, constant table, class variable table, and instance variable table with the module and makes that class the superclass of the class the module is mixed into (and it does that recursively for all modules that were mixed into that module).
答案是:Module#include(或者更确切地说是Module#append_features)合成一个新的类(有时称为include类),该类与模块共享其方法表、常量表、类变量表和实例变量表,并将该类作为模块混合到的类的超类(对于所有混合到该模块的模块,它都会递归执行此操作)。

So, now we know the two pieces of information required to figure out whether or not a method is callable:
因此,现在我们知道了确定方法是否可调用所需的两个信息:

  1. We need to know in which method table, i.e. in which module the method is defined.

  2. 我们需要知道方法定义在哪个方法表中,即在哪个模块中。

  3. We need to know what the chain of superclasses is for the receiver.

  4. 我们需要知道接收器的超类链是什么样的。

Once we know those two things, we can check whether the module the method is defined in is part of the lookup chain of the receiver.
一旦我们知道这两点,我们可以检查方法定义所在的模块是否是接收器查找链的一部分。

Answering #1 is easy. We can use Object#method to create a Method object (a reflective proxy) for the method we want to know about, and then we can use Method#owner to ask the method where it is defined.
回答#1很容易。我们可以使用Object#method为我们想了解的方法创建一个Method对象(反射代理),然后我们可以使用Method#owner来询问方法在哪里定义。

So, we know that calling the method at the top-level works, which means we can ask the top-level object for the method:
所以,我们知道在顶层调用方法是有效的,这意味着我们可以向顶层对象询问该方法:

m = method(:lc)

And now we can ask the method for its owner:
现在,我们可以询问方法的所有者:

m.owner
#=> #<Class:#<Object:0x0000000104c3cd00>>

Uh. Okay. That's not very helpful. What this tells us is the following:
嗯。好吧。这并不是非常有帮助。这告诉我们以下内容:

  • The method is defined in a singleton class.

  • 该方法在单例类中定义。

  • The object the singleton class belongs to is a direct instance of Object.

  • 单例类所属的对象是Object的直接实例。

  • And that's it.

  • 就是这样。

So, all we know is that the method is defined in a singleton class of some object. We have to dig a bit deeper.
因此,我们只知道该方法在某个对象的单例类中定义。我们需要深入挖掘一下。

One possibility would

英文:

> Does anyone know why the method is 'defined' outside of the class but not inside?

The method lookup algorithm in Ruby is relatively simple. (At least it was until the introduction of Module#prepend / Module#prepend_features, so let's ignore that for now). Here's how it goes:

  1. Retrieve the value of the hidden class pointer of the receiver.
  2. Check whether the method table contains the method.
    1. If yes, you are done.
    2. If not, continue to step #3.
  3. Retrieve the value of the superclass pointer.
    1. If there is no superclass, go to step #4.
    2. If there is a superclass, repeat from step #2.
  4. Start the algorithm from the beginning with the message selector method_missing, passing the name of the original message and its arguments as arguments.

And that's it. This is always guaranteed to terminate because the top-level class always has a method named method_missing.

Now, you might ask: but wait, what about singleton classes?

The answer is: the hidden class pointer of an object points to its singleton class, and the singleton class's superclass pointer points (directly or indirectly) to the "actual" class.

Now, you might ask: but wait, why doesn't Object#class return the singleton class, then?

The answer is: Object#class doesn't directly return the hidden class pointer. Instead, it follows the superclass chain until it finds the first "normal" class.

Now, you might ask: but wait, what about mixins using Module#include?

The answer is: Module#include (or rather Module#append_features) synthesizes a new class (sometimes called an include class) which shares its method table, constant table, class variable table, and instance variable table with the module and makes that class the superclass of the class the module is mixed into (and it does that recursively for all modules that were mixed into that module).

This "trick" of inserting singleton classes and include classes into the normal class chain means that operations which happen often are fast and simple, namely method lookup. The complexity is moved to operations like calling Object#class or Module#include, but that's okay because those are only rarely called and typically only happen during debugging or application startup.

So, now we know the two pieces of information required to figure out whether or not a method is callable:

  1. We need to know in which method table, i.e. in which module the method is defined.
  2. We need to know what the chain of superclasses is for the receiver.

Once we know those two things, we can check whether the module the method is defined in is part of the lookup chain of the receiver.

Answering #1 is easy. We can use Object#method to create a Method object (a reflective proxy) for the method we want to know about, and then we can use Method#owner to ask the method where it is defined.

So, we know that calling the method at the top-level works, which means we can ask the top-level object for the method:

m = method(:lc)

And now we can ask the method for its owner:

m.owner
#=&gt; #&lt;Class:#&lt;Object:0x0000000104c3cd00&gt;&gt;

Uh. Okay. That's not very helpful. What this tells us is the following:

  • The method is defined in a singleton class.
  • The object the singleton class belongs to is a direct instance of Object.
  • And that's it.

So, all we know is that the method is defined in a singleton class of some object. We have to dig a bit deeper.

One possibility would be to use Method#source_location to find where the source code of the method is. However, this not guaranteed to work: for example, methods in C extensions for YARV or methods written in Java in JRuby or TruffleRuby do not have "(Ruby) source code", so this method will return nil.

In this case, the method will work and will give the path to the file inside the Gem and the line number where the method is defined.

However, let's see if we can find out more. Every object in Ruby has the Object#inspect method, whose purpose it is to provide human-readable debug information. Well, we are debugging and we are humans (I assume), so let's see what we can get.

We shouldn't be too hopeful, because for a typical singleton method, that will look something like this:

foo = Object.new
def foo.bar(a, b = 23, *c, d, e:, f: 42, **g, &amp;h) end

b = foo.method(:bar)
b.inspect
#=&gt; &#39;#&lt;Method: #&lt;Object:0x0000000109053ca0&gt;.bar(a, b=..., *c, d, e:, f: ..., **g, &amp;h) /path/to/source/file.rb:2&gt;&#39;

Which basically just gives us the same information we can also get from Method#owner, Method#source_location, and Method#parameters, but in a nice and compact way.

But let's try it anyway:

m.inspect
#=&gt; &#39;#&lt;Method: main.lc(text, to, from=...) /usr/lib/ruby/gems/3.2.0/gems/language-converter-1.0.0/lib/language_converter.rb:14&gt;&#39;

That's interesting! The method is actually displayed as main.lc(text, to, from=...) and not something like #&lt;Object:0x0000000109053ca0&gt;.lc(text, to, from=...). So, what is main?

main is the informal name of the so-called top-level object, i.e. the object which is self in code that is at the top-level of a script, not enclosed in any module:

toplevel = self

toplevel.inspect
#=&gt; &#39;main&#39;

This means that the method lc is defined in the singleton class of main, something like this:

def self.lc(text, to, from=&#39;en&#39;)
  # …
end

And this, finally, explains why you can't call it from anywhere but the top-level:

lc

works, because the implicit receiver self here is main, the hidden class pointer of main points to main.singleton_class, and that's where lc is defined.

But

t.translate(…)

doesn't work, because inside translate, where lc is called without a receiver, the implicit receiver self is t whose method lookup chain looks something like this:

  1. t's hidden class pointer which is t.singleton_class.
  2. t.singleton_class's hidden superclass pointer which is t.singleton_class.superclass which is t.class which is Translator.
  3. Translator's hidden superclass pointer which is Translator.superclass which is Object.
  4. Object's hidden superclass pointer which is Object's include class for Kernel.
  5. Object's include class for Kernel's hidden superclass pointer which is Object.superclass which is BasicObject.

You can roughly approximate this by using Ruby Reflection:

t.singleton_class.ancestors
#=&gt; [
#     #&lt;Class:#&lt;Translator:0x00000001023c8ef0&gt;&gt;,
#     Translator,
#     Object,
#     Kernel,
#     BasicObject
#   ]

As you can see, main.singleton_class does not appear anywhere in the method lookup chain, and thus, lc will not be found.

If you really, absolutely, have to use this method, there are a few possible workarounds.

You could grab a reference to the method and keep it around for later:

LC = method(:lc)

class Translator
  # …

  def translate text
    LC.(text, @to, @from)
  end
end

You could grab a reference to main and keep it around for later:

MAIN = self

class Translator
  # …

  def translate text
    MAIN.lc(text, @to, @from)
  end
end

In fact, Ruby has a pre-defined constant for the whole top-level Binding called TOPLEVEL_BINDING. You can get the top-level object using the Binding#receiver method:

class Translator
  # …

  def translate text
    TOPLEVEL_BINDING.receiver.lc(text, @to, @from)
  end
end

This is a weird way to define a method. It makes this method very hard and annoying to use.

Typically, methods like this, which are meant to be called at the top-level with an implicit receiver are defined as instance methods of Kernel. Some methods you probably recognize are Kernel#puts or Kernel#require.

In fact, by the way the method is indented in the code, I am not even sure this was intended.

Which brings us to your other question:

> I cannot find the source code on its homepage

As shown above, you can always find the location of the source code of a method by simply asking it:

m.source_location
#=&gt; &#39;/usr/lib/ruby/gems/3.2.0/gems/language-converter-1.0.0/lib/language_converter.rb:14&#39;

So, on my system, the source code for lc is in the file at the path /usr/lib/ruby/gems/3.2.0/gems/language-converter-1.0.0/lib/language_converter.rb starting at line 14. On your system, the path will obviously be slightly different.

The easiest way to get access to the source code of the entire gem would be to use the gem open command:

gem open language-server

This will open the whole gem directory in your default editor.

Another command that is sometimes useful is gem unpack which unpacks an installed gem into the current directory. If you only want to read the source code, that is overkill since the gem is already unpacked into your gem directory when you install it. But if you want to make changes, you obviously shouldn't do this in the gem directory itself.

Please note: if you decide to actually do this and read this gem's source code, please do not learn anything from it, except maybe how not to write Ruby. It is horrible.

huangapple
  • 本文由 发表于 2023年6月1日 11:59:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/76378577.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定