Home > Back-end >  Passing around procs between different objects
Passing around procs between different objects

Time:11-15

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
  • Related