On Sunday, May 16, 2010 07:49:56 pm James H.on wrote:
first = array.first
Not pretty, and I’m sure someone could improve it, but it works.
That it does, that it does. Unfortunately, I don’t understand it. If you’ve
got a minute, would you give a hand?
It looks to me like:
each_join takes three arguments. The first is an array, the second is
predefined to be an empty array, the third is a reference to a block.
The second is defaulted to be an empty array. You can override it –
and the
idea is not for you to override it, but for me to when I call that
method
recursively.
A Proc object lets you assign a block to a variable. My naive reading of
class Proc - RDoc Documentation leads me to imagine that the
first benefit of this is that you can assign what’s essentially a method
call to a variable, almost like you’re making a new object out of the
variable. What does the object do? Take some number of arguments to its
call method, and do something with those arguments.
Why would you want to use this? I don’t know. But it looks like that’s all
it does.
Well, one example would be something like Rake. I can specify a task
like
this:
task :foo do
stuff
end
Rake will then take my block and store it in some data structure, as
part of a
Task object, which tracks its dependencies and everything else. When
it’s time
to actually run that code, it digs it up and calls it.
It’s really quite useful, but you should avoid it when possible – use
yield
and block_given? instead – because it’s slightly more expensive to
actually
convert it to a proc object.
And what does this have to do with each_join?
Because the outer call to each_join never calls ‘yield’. What it’s doing
is
taking that same block and passing it to the inner calls, so that
eventually,
when you get to that first part of the ‘if’ statement (if the array is
empty,
which I should’ve written as ‘if array.empty?’), it will be calling the
same
block you passed in in the first place.
Okay. The second argument, context. I’m not sure what it’s doing. It’s an
empty array which is handed back to the block when the length of the
original argument array is zero. I don’t see how the argument array ever
gets set to zero, though.
Let’s step through it:
Basically, I’m using the Lisp idea of dealing with the first element,
and then
the rest of the array. So, look here:
first = array.first
rest = array[1…array.length]
So, if your array was [1, 2, 3],
first is 1
rest is [2, 3]
Then, look at the actual recursive call:
each_join rest, context+[elem], &block
So here, I’m calling each_join with ‘array’ set to ‘rest’, and I’m also
adding
an element from ‘first’ to the context. So the first call to each_join
has
array as [1, 2, 3]. As it gets deeper, you get a call of [2, 3], then
one with
[3], then one with []. At each step, it’s adding the current element of
first
(from that loop) to the context.
Okay, here goes my explanation:
Why is &block passed in? So that on successive calls to each_join, the
block is correctly associated with the method.
Not “successive”, but recursive.
And then you start iterating over first.
first.each do |elem| #first time around, elem == “bacon”
each_join rest, context+[elem], &block
So what happens now?
first = [“jam”, “bees”, “please”]
rest = [“1”, “2”, “3”]
And, here’s the important point, for the first iteration,
context = [“bacon”].
Then, after that entire inner loops finish, you get called again, like
this:
context = [“time”]
first = [“jam”, “bees”, “please”]
rest = [“1”, “2”, “3”]
And so on.
Let’s follow it to an inner iteration:
context = [“bacon”, “jam”]
first = [“1”, “2”, “3”]
rest = []
And finally, to the place you actually yield:
context = [“bacon”, “jam”, “1”]
array = []
I don’t know if that makes more sense – you probably saw most of this
with
your trace.
It does seem pretty weird, though. Out of curiosity, what do you need
this for?
Okay, you gave me quite an education today, so turn about is fair play.
I have a program which accepts input and outputs output.
It has both a gui and a scripting environment.
[snip]
jet=bam – does something else jetish
and so on. I’m popping each option string into an array:
[[foo=bar, foo=bam], [jet=bar, jet=bam]]
And from there, generate every possible combination of all options. I’d
done everything else, but that last item on the list was killin’ me.
Are all of them required, though? Because that’s the assumption I was
working
with. One way to avoid that might be to add an iteration to each of
these
where you don’t add that item. That is, change the inner loop to:
first.each do |elem|
each_join rest, context+[elem], &block
end
call again, without anything from this array
each_join rest, context, &block
And if you really want to balloon the number of test cases, you could
make
sure it works in any order. That is, when you use this:
each_join(array_collection) do |options|
options.permutation.each do |perm|
# do something with each permutation
end
end
Also, as a mental exercise, you could try re-implementing this without
recursion. I wouldn’t do that unless you actually start having thousands
of
items, though – I would hope you won’t run into stack overflows,
because that
would be an insane number of iterations!
Ok, I thought I’d be done, but this is just bothering me. I’m going to
make it
prettier:
class Array
def each_join
return enum_for(:each_join) unless block_given?
if empty?
yield []
else
array.first.each do |elem|
first = [elem]
array[1...array.length].each_join do |subarray|
yield first + subarray
end
end
end
end
end
I think that still does the same thing. It’s probably less efficient,
but I’m
really not sure. It might be a little easier to understand, though.