Thursday, December 25, 2008

Hard Stuff in Ruby- Part 2: Blocks

So in Hard Stuff Part One we covered some basic metaprogramming tricks. Here, we'll take care of another topic that is a little complicated at first glance, blocks.

It's really not so bad though. But they seem complicated, but just like methods, there are only two places to look at, the method call and the method declaration. That said, there is still some new stuff in both places. Let's take a look at the most common iterator method, each, that "take a block" as a parameter first. This takes care of the "method call" portion.

This is from the ruby docs:

a = [ "a", "b", "c" ]
a.each {|x| print x, " -- " }

Produces:

a -- b -- c --

So |x| is the "block parameter." This has a special context inside of the each method. But what is it doing with the x? So it must be looping through the array somehow, and updating the value for x. Where is this happening? In the method declaration for each:

VALUE
rb_ary_each(ary)
VALUE ary;
{
long i;

for (i=0; ilen; i++) {
rb_yield(RARRAY(ary)->ptr[i]);
}
return ary;
}

There it is, in C. That explains the "rb_" thing for ruby methods. But there's really not much to it. It takes the array as the parameter, loops through it and yields the element to the piece of code that is calling it.

You can also define your own Proc objects with Proc.new and call that Proc with Proc.call. This lets you define a chunk of code that can be reused at different places in your program. Sounds a lot like a method. What's the difference? Well, it's actually an abstraction of a method. So you can use it to make methods. The documentation example is pretty clear.

def gen_times(factor)
return Proc.new {|n| n*factor }
end

times3 = gen_times(3)
times5 = gen_times(5)

times3.call(12) #=> 36
times5.call(5) #=> 25
times3.call(times5.call(4)) #=> 60

Nice. Sort of like a template for methods.

How can I use blocks in Tasker? Well, the problem is how to select a random key in a hash where the values are the relative likelihood of being chosen.

My solution so far has been this:
  
def die_roll(hash)
arr = hash.collect { |x, y| if y and y['weight'] then
a = []
y['weight'].times do
a.push(x)
end
a
else
x
end
}
arr.flatten!
return arr[rand(arr.size)]
end

But I think I can clean it up a bit. Let's clear out the conditionals and see what happens.

def random_from_weighted_hash(hash)
arr = hash.collect { |x, y|
a = []
y['weight'].times do
a.push(x)
end
a
end
}
arr.flatten!
return arr[rand(arr.size)]
end

Oddly, when y is blank, x still gets pushed onto a. But if y['weight'] is nil then the program throws an error. Ok. So let's just define the shell of nil.times as the following.

def nil.times
end

That takes care of the error, but we still have a problem. Now either setting the time but not the weight OR setting the weight as blank will result in the task not being added to the array. What to do...
How about this?

def nil.times(&block)
1.times(&block)
end

Sweet. So nil.times is a method that takes a block as a parameter and passes that block to 1.times.

That's all for now.

Hard stuff list:
-Metaprogramming
-Blocks
-Lambdas
-Singleton
-Method Missing
-eval methods
-bindings

No comments:

Post a Comment