I'm trying to make the following work but I'm obviously missing something:
class Person
def fetch
puts 'Fetch it boy!'
end
def action(data)
data.call
end
end
class Animal
def play
Person.new.action(proc { fetch })
end
end
Animal.new.play # undefined local variable or method `fetch' for #<Animal:0x0000000001b172d0>
My goal is to get Person
execute a method Animal
has chosen. I've also tried using instance_exec
but to no avail.
Now I know I can use eval
and just pass the method name as a string but I'd like to avoid that.
Any ideas?
Thanks in advance
Disclaimer (of sorts):
Please don't propose code restructuring or rewrites. The purpose of the question is to better understand how blocks behave when passed around between objects.
CodePudding user response:
You are looking for:
instance_eval(&data)
object.instance_eval
evaluates block, but replaces self
within that block (which would normally be self
of the context block was created in) with object
:
whoami = proc { self }
whoami.call => main
1.instance_eval(&whoami) => 1
Note however, that instance_eval
also passes an object as an argument to the block, which might be problematic if you want to pass a lambda:
whoami = lambda { self }
1.instance_eval(&whoami) #=> ArgumentError (wrong number of arguments (given 1, expected 0))
There is another similar method: instance_exec
. Not only it does not pass self as an argument:
whoami = lambda { self }
1.instance_exec(&whoami) #=> 1
but it additionally allows you to pass other arguments:
add_number = lambda { |number| self number }
1.instance_exec(3, &add_number) #=> 4
Naturally, both methods need to be used with extra caution and very sparingly - they are nice when you want to declare class-wide hooks in a declarative manner to be executed on each instance. They should not be used as a mean of interaction between two objects, unless you really know what you are ding and can justify it does not validate encapsulation.
CodePudding user response:
TL;DR
Firstly, your originally-posted code didn't define the proper relationship or collaboration between your objects, which is part of the problem. Secondly, the way one needs to capture and forward blocks is often rather non-intuitive, so the reasoning behind doing this with blocks rather than simple dependency injection (e.g. initializing a new Person with a specific Animal to collaborate with) or message-passing makes this a bit harder than it needs to be. Simple is usually better, and often easier to debug and extend later on.
That said, the following redesign is semantically clearer, but also shows how to forward blocks as Proc objects between collaborators. Person now takes an optional block, and when present passes that block as a Proc object to a method on Animal where it can be called.
Your Code Redesigned
Consider the following redesign, which strives to attach the right behavior to the right objects:
class Person
def speaks_to_animal species, phrase, &reaction
animal = Animal.new species
return animal.reacts_to(phrase, &reaction) if block_given?
animal.reacts_to phrase
end
end
class Animal
attr_reader :species
def initialize species
@species = species.to_s
end
def reacts_to phrase
case species
when 'lion'
pp "The #{species} eats you."
when 'dog'
block_given? ? yield : pp("The #{species} barks at you.")
else
pp "The #{species} doesn't know what to do."
end
end
end
In particular, the goal here is to redesign the code such that Person#speaks_to_animal while Animal#reacts_to_phrase spoken by a Person. This keeps the behavior where is belongs, insofar as a Person doesn't #fetch and an Animal shouldn't have to know anything about the internals of the Person object to collaborate.
As a byproduct, this redesign provides greater flexibility. Your blocks are now optional, and while they are passed to a Person they are then forwarded and called by the Animal, which seemed to be the intent of your original code. You now interact with an Animal through a Person, and that Person can talk to any type of Animal species you choose to specify without the need to subclass Animal or hard-code reactions. For example:
person = Person.new
person.speaks_to_animal :dog, "Fetch, boy!"
person.speaks_to_animal(:dog, "Fetch, boy!") { pp "The dog brings you back the stick." }
person.speaks_to_animal :lion, "Fetch, boy!"
If you don't pass a block to person, then the dog doesn't know what to do, and just barks at you. If you pass a behavioral expectation as a block, that block gets forwarded to animal#reacts_to where it's called via yield
. Of course, if you ask a lion to play fetch, bad things are going to happen.
Repackaging the behavior and relationship between the objects allows you to do all sorts of things, like keying off elements of the phrase spoken by person to enable more complex responses, or allowing animal to key off elements of the phrase to respond differently based on its species. Mostly, though, this new code solves the problem of how to pass an optional block representing the Animal's reaction without coupling the objects too tightly.
Semantically, a Person should know what Animal they're talking to, and what they hope the Animal will do in response. Whether passing a block around is really the best way to represent the Person's expectations or the Animal's reactions is more arguable, and I would personally opt to focus more on defining reactions based on species and phrase rather than on passing Proc objects. Your mileage may vary.
CodePudding user response:
Here's the option that keeps things simple:
class Animal
def play
Person.new.fetch
end
end
If you really need to pass a method name around as a value, the most natural thing is to use is probably a symbol (not a string):
class Person
def fetch
puts 'fetched'
end
def action(a)
self.send(a)
end
end
class Animal
def play
Person.new.action(:fetch)
end
end
Animal.new.play
Or you can do it this way instead:
class Person
def fetch
puts 'fetched'
end
def action(a)
a.bind(self).call
end
end
class Animal
def play
fetch = Person.instance_method(:fetch)
Person.new.action(fetch)
end
end
CodePudding user response:
In my opinion the organization is not correct.
Still even with this solution the animal is controlling the person, while we generally assume the Person is going to initiate the command to fetch.
class Person
def initialize(name = 'Happy Pet Owner', pet = Animal.new)
@name = name
@pet = pet
end
def fetch
'Fetch it boy!'
end
def action(&block)
print "#{@name} says: ", fetch
block.call
end
end
class Animal
def play
Person.new.action { puts " Fido chases ball." }
end
end
Animal.new.play