Example:
7.cars.2.doors
I will add the cars method to Integer and it will return a CarBuilding object.
This CarBuilding object then needs to receive the message 2 which will do some processing and return itself.
Finally doors will be called on this CarBuilding object
Any kind of fancy Meta Programming able to do this?
Note: I don't want to pass parameters. Just use method chaining.
No, this isn't possible.
Using normal message sending syntax, the message must be a valid identifier. 2 is not a valid identifier, identifiers can't start with a digit. This is simply not syntactically legal.
It is possible to define a method named 2 using metaprogramming. Note, however, that this is the name 2 and not the Integer 2. Obviously, it is not possible to invoke such a method using normal message sending syntax (nor is it possible to define it using normal method definition syntax).
class CarBuilding
define_method(:'2') do
CarBuildingWithDoors.new(2)
end
end
7.cars.public_send(:'2').doors
Because names started with digit are not valid you can use underscore:
class CarBuilding
def method_missing(method, *args, &block)
raise NoMethodError unless method =~ /\A_\d*\z/
puts "do something with #{method[1..-1].to_i}"
return self
end
def doors
puts "doors"
end
end
class Integer
def cars
return CarBuilding.new
end
end
puts 7.cars._2.doors
Related
I'm learning ruby, and noticed that I cannot create a class method called puts:
class Printer
def initialize(text="")
#text = text
end
def puts
puts #text
end
end
The error is:
`puts': wrong number of arguments (given 1, expected 0)
My expectation was that I could use the code like this:
p = Printer.new("hello")
p.puts
It's not just because puts is a built-in method, though. For instance, this code also gives a syntax error:
def my_puts(text)
puts text
end
class Printer
def initialize(text="")
#text = text
end
def my_puts
my_puts #name
end
end
tldr; within the scope of the instance, the puts resolves to self.puts (which then resolves to the locally defined method, and not Kernel#puts). This method overriding is a form of shadowing.
Ruby has an 'implicit self' which is the basis for this behavior and is also how the bare puts is resolved - it comes from Kernel, which is mixed into every object.
The Kernel module is included by class Object, so its methods [like Kernel#puts] are available in every Ruby object. These methods are called without a receiver and thus can be called in functional form [such as puts, except when they are overridden].
To call the original same-named method here, the super keyword can be used. However, this doesn't work in the case where X#another_method calls X#puts with arguments when it expects to be calling Kernel#puts. To address that case, see Calling method in parent class from subclass methods in Ruby (either use an alias or instance_method on the appropriate type).
class X
def puts
super "hello!"
end
end
X.new.puts
P.S. The second example should trivially fail, as my_puts clearly does not take any parameters, without any confusion of there being another "puts". Also, it's not a syntax error as it occurs at run-time after any language parsing.
To add to the previous answer (https://stackoverflow.com/a/62268877/13708583), one way to solve this is to create an alias of the original puts which you use in your new puts method.
class Printer
alias_method :original_puts, :puts
attr_reader :text
def initialize(text="")
#text = text
end
def puts
original_puts text
end
end
Printer.new("Hello World").puts
You might be confused from other (static) programming languages in which you can overwrite a method by creating different signatures.
For instance, this will only create one puts method in Ruby (in Java you would have two puts methods (disclaimer: not a Java expert).
def puts(value)
end
def puts
end
If you want to have another method with the same name but accepting different parameters, you need to use optional method parameters like this:
def value(value = "default value")
end
I am trying to write this inside my class:
class << self
def steps
#steps.call
end
def transitions
#transitions.call
end
def steps(&steps)
#steps = steps
end
def transitions(&transitions)
#transitions = transitions
end
end
That won't work since in Ruby, I can't do this kind of method overloading. Is there a way around this?
You can kind of do this with method aliasing and mixins, but the way you handle methods with different signatures in Ruby is with optional arguments:
def steps(&block)
block.present? ? #steps = block : #steps.call
end
This sort of delegation is a code smell, though. It usually means there's something awkward about the interface you've designed. In this case, something like this is probably better:
def steps
#steps.call
end
def steps=(&block)
#steps = block
end
This makes it clear to other objects in the system how to use this interface since it follows convention. It also allows for other cases, like passing a block into the steps method for some other use:
def steps(&block)
#steps.call(&block)
end
Ruby does not support method overloading (see "Why doesn't ruby support method overloading?" for the reason). You can, however, do something like:
def run(args*)
puts args
end
args will then be an array of the arguments passed in.
You can also pass in a hash of options to handle arguments, or you can pass in nil when you don't want to supply arguments and handle nil in your method body.
def doSomething(value)
if (value.is_a?(Integer))
print value * 2
else
print "Error: Expected integer value"
exit
end
end
Can I tell a Ruby method that a certain parameter should be an Integer, otherwise crash? Like Java.
No, you can't. You can only do what you're already doing: check the type yourself.
I'm late to the party, but I wanted to add something else:
A really important concept in Ruby is Duck Typing. The idea behind this principle is that you don't really care about the types of your variables, as far as they can do what you want to do with them. What you want in your method is to accept a variable that responds to (*). You don't care about the class name as far as the instance can be multiplied.
Because of that, in Ruby you will see more often the method #responds_to? than #is_a?
In general, you will be doing type assertion only when accepting values from external sources, such as user input.
I would suggest a raise unless type match at the beginning of the method
def do_something(value)
raise TypeError, 'do_something expects an integer' unless value.kind_of?(Integer)
...
end
This is raise an error and exit unless value is an Integer
You can raise an Exception anytime arbitrarily if you deem it necessary.
def doSomething(value)
if (value.is_a?(Integer))
print value * 2
else
raise "Expected integer value"
end
end
Whether or not you really want to do this is a separate issue. :)
Ruby doesn't have parameter type verification, though you can add sugar method to all objects for convenience to verify type like this:
def doSomething(value)
print value.should_be(Numeric) * 2
end
or
def initialize(fruit)
#fruit = fruit.should_be(Fruit)
end
Object.class_eval do
def should_be cls
hide_from_stack = true
if self && !self.is_a?(cls)
raise("Expected class #{cls}, got #{self.class}")
end
self
end
end
I understand that method_missing is something of a last resort when Ruby is processing messages. My understanding is that it goes up the Object hierarchy looking for a declared method matching the symbol, then back down looking for the lowest declared method_missing. This is much slower than a standard method call.
Is it possible to intercept sent messages before this point? I tried overriding send, and this works when the call to send is explicit, but not when it is implicit.
Not that I know of.
The most performant bet is usually to use method_missing to dynamically add the method being to a called to the class so that the overhead is only ever incurred once. From then on it calls the method like any other method.
Such as:
class Foo
def method_missing(name, str)
# log something out when we call method_missing so we know it only happens once
puts "Defining method named: #{name}"
# Define the new instance method
self.class.class_eval <<-CODE
def #{name}(arg1)
puts 'you passed in: ' + arg1.to_s
end
CODE
# Run the instance method we just created to return the value on this first run
send name, str
end
end
# See if it works
f = Foo.new
f.echo_string 'wtf'
f.echo_string 'hello'
f.echo_string 'yay!'
Which spits out this when run:
Defining method named: echo_string
you passed in: wtf
you passed in: hello
you passed in: yay!
After I have created a serious bunch of classes (with initialize methods), I am loading these into IRb to test each of them. I do so by creating simple instances and calling their methods to learn their behavior. However sometimes I don't remember exactly what order I was supposed to give the arguments when I call the .new method on the class. It requires me to look back at the code. However, I think it should be easy enough to return a usage message, instead of seeing:
ArgumentError: wrong number of arguments (0 for 9)
So I prefer to return a string with the human readable arguments, by example using "puts" or just a return of a string. Now I have seen the rescue keyword inside begin-end code, but I wonder how I could catch the ArgumentError when the initialize method is called.
Thank you for your answers, feedback and comments!
It is possible to hook into object creation by overriding the Class#new method e.g.
class Class
# alias the original 'new' method before overriding it
alias_method :old_new, :new
def new(*args)
return old_new(*args)
rescue ArgumentError => ae
if respond_to?(:usage)
raise ArgumentError.new(usage)
else
raise ae
end
end
end
This overriden method calls the normal new method but catches ArgumentError and if the class of the object being created provides a usage method then it will raise an ArgumentError with the usage message otherwise it will reraise the original ArgumentError.
Here is an example of it in action. Define a Person class:
class Person
def initialize(name, age)
end
def self.usage
"Person.new should be called with 2 arguments: name and age"
end
end
and then try and instantiate it without the required arguments:
irb(main):019:0> p = Person.new
ArgumentError: Person.new should be called with 2 arguments: name and age
from (irb):8:in `new'
from (irb):22
Note: this isn't perfect. The main problem being that it is possible that the ArgumentError we catch has been caused by something other than an incorrect number of arguments being passed to initialize which would lead to a misleading message. However it should do what you want in most cases.