Wednesday, June 11, 2008

(Road to) Partition

I'm a big fan of Ruby's Enumerable#partition method. It's very functional, and comes in quite handy. Here's the basic deal: it takes a block and returns two Arrays. The first of which contains all elements for which the block is truthy, and the second of which contains all elements for which the block is false. Here's some code that more-or-less duplicates how it works:


module Enumerable

def my_partition(&block)
return select(&block), reject(&block)
end

end


It returns a comma-separated list, so you can pull the returned values out into two separate Arrays, use the * operator to cram them together, what have you.



Before


Here's some Rails code in its initial quick 'n dirty state. One of the models in the app is called Enrollment, and restore_previous_state! is basically an undo method for some operations on enrollments. Methods run earlier in this app either set the ready_for_review boolean flag to true, or autovivified some enrollments entirely. So this takes in some enrollments, destroys them if they were autovivified, and just marks those that it didn't create itself as unready.

I also wanted to keep track of how many changes of each type were done, for reporting purposes. Hence the += 1 counter operations. Then I spit out the results Hash at the end to know what I did. I think this version is fairly procedural for Ruby code, at least for my usual style. That's not necessarily bad, although I do prefer the changes below.


def restore_previous_state!(enrollments_to_modify)
results = { :reset => 0, :destroyed => 0 }
enrollments_to_modify.each do |e|
if e.autovivified?
e.destroy
results[:destroyed] += 1
else
e.ready_for_review = false
e.save
results[:reset] += 1
end
end
results
end



After


Here's the same method after some functionally-oriented refactorings that use the partition method.


def restore_previous_state!(enrollments_to_modify)
to_destroy, to_reset = enrollments_to_modify.partition(&:autovivified?)
to_destroy.map(&:destroy)
to_reset.map { |e| e.ready_for_review = false; e.save }
{ :reset => to_reset.size, :destroyed => to_destroy.size }
end


In the new version, I partition the enrollments based on whether or not they were autovivified, because (as we already know) my method has to behave quite differently in each case. Then I just map the destroy operation onto all the enrollments that I should destroy, and a combined unready & save for the others. Since to_destroy still exists in memory after the destroy operations (it just had its database records deleted), I can still get its size. And of course to_reset is still around, just with some readiness changes.

Presto, spit out an anonymous Hash reporting how many changes of each type, and the external behavior of the method stays entirely the same, which is as it should be for a refactoring. The new version trims some lines (14 -> 6), and I also find it more readable.




Elsewhere, the app goes even further with partition. I won't explain this in as much detail, because if you followed the stuff above, this should be fairly clear as an additional example.

One curious thing about this is that it uses a state-changing operation (save) as the partitioning condition, so it becomes a try to save, and separate based on whether or not it worked composite operation. Also, since I only need to keep counts, I immediately map the size method onto each sub-Array that comes out of partition.

My co-worker Jim calls this sort of code dense. I prefer compact. He assures me that he was just teasing me.


def save_cleanup_and_report!(post_delayed_rules_to_save, rejected_potentials)
ready_to_save, invalid_for_saving = post_delayed_rules_to_save.partition(&:valid?)
actually_saved_count, failed_save_count = ready_to_save.partition(&:save).map(&:size)
invalid_for_saving.map(&:destroy)
rejected_potentials.map { |e| e.ready_for_review = false; e.save! }
[actually_saved_count, (rejected_potentials.size + failed_save_count)]
end


I apologize for the pun in the title, of course.

No comments: