April 9

On caching expensive case conditions

Posted by mtoledo
Filed under ruby | No Comments

Ruby has a pretty expressive and flexible case statement. It actually differs from most other language’s ’switch’ statements in that it auto-breaks on each condition instead of falling through.

// C

switch (x) {
case 1:
doSomething();
break;
case 2:
doSomethingElse();
break;
}

# ruby

case x
when 1: do_something
when 2: do_something_else
end

I do think not having to break was the right decision, but not everyone agrees and it does have some shortcomings like the one posted here: . Many times, shortcomings in ruby will happen in operators you can’t override to bend to your will.

One such case is when you want to cache expensive operations in a case statement. Suppose you’ll have a case statement similar to the above, but with very expensive operations.

case x
when 1: do_something_that_takes_a_long_time
when 2: do_something_else_that_takes_a_long_time
end

Now, assume that that case statement is going to be called very frequently, and that the expensive methods’ response will continue to be the same through time. There are more options for doing that than it seems like at a glance.

The first option is just adding an @@array which stores the expensive operation, and look it up on your case statement.

@@lookup_array = [do_something_that_takes_a_long_time,  do_something_else_that_takes_a_long_time]
case x
when 1: @@lookup_array[0]
when 2: @@lookup_array[1]
end

This drawback to this approach is that you segregate the condition from the behavior. The solution would be to lazily initialize those values. Then they’ll be displayed just beside their condition.

@@lookup_hash = {}
case x
when 1: @@lookup_hash[1] ||= do_something_that_takes_a_long_time
when 2: @@lookup_hash[2] ||= do_something_else_that_takes_a_long_time
end

Of course one of the drawbacks here is that sometimes you don’t want to lazily initialize an expensive operation, since this means the first consumer of each conditions waits an intolerable time. Another drawback is that if your conditions are not numbers, you can’t use a hash for them, and doing lookups become convoluted. To fix those you’d have to do something like this.

@@lookup_array = [do_something_that_takes_a_long_time, nil]
@@conditions = [1..3, 4..6]
case x
when conditions[0]: @@lookup_array[0]
when conditions[1]: @@lookup_array[1] ||= do_something_else_that_takes_a_long_time
end

In this case I did both options: lazily initializing it so its more readable, or starting it up in the beginning but without any easy way to get from the condition to the operation that is cached for it. None of the options really satisfies and, and of course the fact that the condition is also stored someplace else makes this approach nothing more than something curious to come up with as it really can’t be used in a real situation.

So, what could be a good solution for this is to just use a hash instead of a case statement altogether, and then doing the lookup through it.

@@lookup_hash = {
1 => do_something_that_takes_a_long_time,
2 => do_something_else_that_takes_a_long_time
}

@@lookup_hash[x]

The code above totally replaces the case statement of the first example, while keeping the condition in a nice readable way close to the operation and at the same time caching the expensive operations without lazily initializing them, just like we wanted.

But what about the situations where the lookup can’t be done through a hash key, like if it was a range rather than a number?

@@lookup_hash = {
1..3 => do_something_that_takes_a_long_time
4..6 => do_something_else_that_takes_a_long_time
}

@@lookup_hash.detect {|k,v| break v  if k === x } # thx coderrr ;)

With the snippet above, which you can encapsulate in a different function, a monkey patch or whateve you prefer, you can basically simulate the functionality of the case statement’s ‘when’ clause, but with the flexibility of not only caching your expensive operation, but also being able to do all sorts of manipulations you can do on a hash (that you can’t do on a case statement), like adding conditions in runtime or recalculating those conditions.

Of course this has the drawback of you not being able to do overlapping conditions, since hashes in ruby 1.8 don’t respect insertion order. This will be fixed in ruby 1.9 but if you really mustdo this in ruby 1.8 you can use a bi-dimensional array.

@@lookup_array = [
[1..3, do_something_that_takes_a_long_time],
[4..6, do_something_else_that_takes_a_long_time]
]

@@lookup_array.detect {|k,v| break v if k === x }

This approach also allows you to easily change your manipulation of the hash so you do a fallthrough like a C style switch statement, which is impossible to do with a vanilla ruby case statement.

Doing this sort of metaprogramming in ruby is quite easy in some situations, quite a bit harder on others. Luckly, ruby is powerful enough that you don’t have to resort to those things most of the time, an so that you can resort to them if you need to.

Update: In the last example, you can actually use a |k, v| param to the block even if its being yielded an array, and it will automatically map each of the indexes of the array to each parameter.

This entry was posted on Thursday, April 9th, 2009 at 9:22 pm and is filed under ruby. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

Leave a Reply