Why can't I override the * method in Ruby?

I am working on a Ruby Gem for creating presentations , and I would like to create a syntax for defining slides that is simple and intuitive. I use instance_eval so that I can call methods myself. Here is what I originally planned to do:

 slide { title 'What is Ruby?' * 'a programming language' * 'with lots of interpreters' * 'lots of fun!' } 

Even if I defined method * , I get an error:

in `instance_eval ': ... syntax error, unexpected' \ n ', expecting :: or' ['or'. '(SyntaxError)

I compromised creating a short method called b to create a bullet, but this is not so nice:

 slide { title 'What is Ruby?' b 'a programming language' b 'with lots of interpreters' b 'lots of fun!' } 

Is this just a translation restriction? Or is there a way around this?

Update:. If you want, you can paste the full source code , but here is a small example of how this is implemented:

 class Slide attr_accessor :title, :bullets end class SlidesDSL attr_accessor :slides def slide @slides ||= [] s = SlideDSL.new s.instance_eval(&block) @slides << s.slide end class SlideDSL def slide @slide ||= Slide.new end def title(text) slide.title end def *(text) bullet(text) end def b(text) slide.bullets ||= [] slide.bullets << text end end end # load_slides_from_file source = File.read(filename) dsl = SlidesDSL.new dsl.instance_eval(source, filename, 0) @slides = dsl.slides 
+4
source share
5 answers

It seems that you rely on syntactic sugar, which is given * a method for many things.

This is not true. You can do it:

 class Dog private def do_stuff(arg) puts 2 + arg end end d = Dog.new d.instance_eval do do_stuff(3) end --output:-- 5 
  • instance_eval() changes itself to the receiver, d in this case.
  • In Ruby, private means that you cannot call a method with an explicit receiver.
  • A method call without an explicit receiver implicitly uses self as the receiver.

Now, if you change the method name from do_stuff to * :

 class Dog private def *(arg) puts 2 + arg end end d = Dog.new d.instance_eval do *(3) end --output:-- 1.rb:13: syntax error, unexpected '\n', expecting tCOLON2 or '[' or '.' 

Thus, op relies on the normal operation of the method, and not on the syntactic sugar attributed to * . Inside the instance_eval block, you expect Ruby to implicitly execute:

 self.*(3) 

which is equivalent to:

 d.*(3) 
+4
source

Yes, this is a limitation of Ruby grammar. In particular, you cannot, as the saw points out, use it with an implicit receiver (and not turn into a unary operator): it expects something on the left side.

All statements are simply methods called on the object referenced earlier, but some statements are more equal than others. Most methods accept an implicit receiver, but one called * does not.

I chose o in a similar situation.

- Added later (since I originally commented on the 7stud post):

The problem is that the Ruby parser (Yacc grammar + a bunch of methods) simply does not allow the line beginning with * to be parsed so that * denotes a method call. If the line starts with *, the only possible parsing is where * is the splat operator. This restriction is unique to the * character used as the method name.

+3
source

If you are ready to use - (or + ) instead of * , you can achieve a very similar result by overloading the unary minus (or plus) to String . The following code automatically detects undefines unary - in a line, so it works during a block, but does not change any definition when a block is completed. Note that if the method is called inside the block, it will still use custom behavior. This is not a big problem since there is no default definition for unary for strings. It can also be useful for extracting common code.

 class Slide attr_accessor :title, :bullets end class SlidesDSL attr_accessor :slides def slide(&block) @slides ||= [] s = SlideDSL.new old_unary_minus = String.instance_method(: -@ ) rescue nil begin String.send(:define_method, : -@ ) do sb(self) end s.instance_eval(&block) ensure String.send(:remove_method, : -@ ) if old_unary_minus String.send(:define_method, : -@ ) do old_unary_minus.bind(self).call end end end @slides << s.slide end class SlideDSL def slide @slide ||= Slide.new end def title(text) slide.title = text end def *(text) bullet(text) end def b(text) slide.bullets ||= [] slide.bullets << text end end end 

Usage example:

 SlidesDSL.new.slide do title "Some title" - "one value" - "another value" 4.times do |i| - "repeated value #{i}" end end 

Return:

 [#<Slide:0x007f99a194d0f8 @bullets=["one value", "another value", "repeated value 0", "repeated value 1", "repeated value 2", "repeated value 3"], @title="Some title">] 
+2
source

It seems that method * should be "anti-private." Using the predefined method * by numbers, you can observe this.

 3.instance_eval{*} # => syntax error, unexpected '}', expecting '=' 3.instance_eval{*()} # => syntax error, unexpected '}', expecting :: or '[' or '.' 3.instance_eval{self.*} # => wrong number of arguments (0 for 1) 

It appears that * is tough to require an explicit receiver.

0
source

You seem to rely on the syntactic sugar that is given to method * for many things.

If you write your method * and use it implicitly . , to indicate that this message is to the recipient, you may be lucky.

For example, when I do this with Fixnum

 class Fixnum def * "This is the modified star" end end 

When I try to do this in IRB

 >> 1 * 1 ArgumentError: wrong number of arguments (1 for 0) 

I would also have a normal problem if it recognized * , if I just hit enter ... he would expect more input on the next line ...

However, if I do this:

 >> 1.* => ""This is the modified star" 

So the point is not that it is impossible to do, you can simply fight the syntactic sugar that develops around this particular symbol.

Consider a private method that you can do, but you will have difficulty using the eval call family. You could do this:

 some_instance.send(:*) 

If, of course, an argument is needed, we can do some_instance.send(:*, argument)

0
source

Source: https://habr.com/ru/post/1498773/


All Articles