fullstackplus
fullstackplus

Reputation: 1071

Returns from a series of calls within a block in Ruby

I have a DSL - like piece of Ruby code that looks like this:

Activity.new('20-06-2012') do
  Eat.log do |l|
    l.duration = 0.30
    l.priority = 5
  end
  Work.log do |l|
    l.duration = 2
    l.priority = 3
  end
end

Every time the log() method is called, a Log object is instantiated (behind the scenes) with the block passed to the method (the block is passed on to the Log object’s constructor). My question is, is there a way to collect all the returns from the log() method? In the example above, the return value of the outermost block is the last call to log(). But I want to get all the calls’ results in an array, not just the last one.

Thanks!

Upvotes: 3

Views: 79

Answers (6)

tokland
tokland

Reputation: 67860

Your internal DSL is still too Ruby-like, you may refactor it to look something like this:

activity '20-06-2012' do
  log :eat do
    duration 0.30
    priority 5
  end
  log :work do
    duration 2
    priority 3
  end
end

Now capture the calls in your instance_eval and accumulate the values in an internal array, the usual DSL stuff.

Upvotes: 3

Matthew Ratzloff
Matthew Ratzloff

Reputation: 4623

Create a new object, ActivityLog or whatever, and pass it to the block from the Activity initializer.

Activity.new('20-06-2012') do |log|
  log.eat do |l|
    l.duration = 0.30
    l.priority = 5
  end
  log.work do |l|
    l.duration = 2
    l.priority = 3
  end
end

If you only have a handful of log types, just add them to ActivityLog (ActivityLog#eat, ActivityLog#work, etc.). Initialize the corresponding object (Eat, Work) and call its log method with the received block.

As a next step, you could add them to an array and iterate through it in ActivityLog#initialize and dynamically create each method.

The lazy way is for ActivityLog to have a method_missing that accepts any method, converts it to a class constant, and then does the above steps. This is bad because it typically makes it harder to debug, you can get misleading exceptions if you call a method on the object that doesn't correspond to a logger, and you can't call methods against the object and see the methods listed.

Hope that helps.

Upvotes: 1

Gareth
Gareth

Reputation: 138042

If (and only if) your log structure is simple and consistent, you could get away with something like this:

Activity.new('20-06-2012') do
  logs = {
    Eat => {:duration => 0.30, :priority => 5},
    Work => {:duration => 2, :priority => 3}
  }

  logs.map do |log_type, log_data|
    log_type.log do |l|
      l.duration = log_data[:duration]
      l.priority = log_data[:priority]
    end
  end
end

You'll need to make your own mind up about whether this fits your structure and whether it's readable enough

Upvotes: 0

J-_-L
J-_-L

Reputation: 9177

The last statement of the block is always the return value. If you don't want to force your users to write l as the last statement in the block, you should use the modifications done to the l instance (whatever it is), instead of the blocks return value. Another option is to change the setter-behavior of the l object to return an array of all values on assignment - but I would go with the first option, depending on your code, it might look like this:

class Log
  attr_accessor :duration, :priority
  def initialize(block)
    block.call(self)
  end
end

Log.new proc{ |l| l.duration = 0.3; l.priority = 5 }
# => #<Log:0x000000029d8030 @duration=0.3, @priority=5>

Upvotes: 1

Gareth
Gareth

Reputation: 138042

André's approach will work, but it's not very readable. If you go back to it in a month or so, it's tricky to spot that comma hiding in there.

Personally I'd make the array collection more explicit:

Activity.new('20-06-2012') do
  logs = []
  logs << Eat.log do |l|
    l.duration = 0.30
    l.priority = 5
  end
  logs << Work.log do |l|
    l.duration = 2
    l.priority = 3
  end
  logs
end

This way, it's very hard to look at the code and misread what it does

Upvotes: 0

Andr&#233; Medeiros
Andr&#233; Medeiros

Reputation: 810

use return, separating each of the blocks with a comma.

Activity.new('20-06-2012') do
  return_value = Eat.log do |l|
    l.duration = 0.30
    l.priority = 5
  end, Work.log do |l|
    l.duration = 2
    l.priority = 3
  end
return return_value #redundant
end

when you return multiple values separated by a comma, Ruby joins them into one array.

Upvotes: 0

Related Questions