Search code examples
ruby-on-railsrubylispscopetap

Dynamic Variable Scoping in Ruby 1.9


I'm interested in using dynamic (as opposed to lexical) scoped variables in Ruby.

It's seems there isn't a direct built-in way, as with let in Lisp. One possible way to do dynamic scoped variable is suggested by Christian Neukirchen. He creates a "thread local hash" in his Dynamic class. I wasn't too crazy about that.

Then I remembered that Ruby 1.9 has a tap method. I see a lot of people using tap to print debugging values in a chain of commands. I think it can be used to very nicely mimic a dynamically scoped variable.

Below is an an example of a situation in which one would want to use a dynamical scoped variable, and a solution using tap.

If I had a blog to post this on, and get some feedback, I would do it there. Instead I come to S/O for criticism of this idea. Post your critique, I'll give the correct answer to the one with the most upvotes.


Situation

You have an ActiveRecord object representing an Account, each account has_many Transactions. A Transaction has two attributes:

  • description
  • amount

You want to find the sum of all transactions on the account, keeping in mind that amount can be either nil or a Float (no you can't critique that).

Your first idea is:

def account_value
  transactions.inject(0){|acum, t| acum += t.amount}
end

This bombs the first time you have a nil amount:

TypeError: nil can't be coerced into Fixnum

Clean Solution

Use tap to temporarily define amount = 0. We only want this to be temporary in case we forget to set it back and save the transaction with the 0 value still in place.

def account_value
  transactions.inject(0){|acm, t| t.amount.tap{|amount| amount ||=0; acm+=amount}; acm}
end

Since the assignment-to-zero-if-nil of amount is within the tap bock, we don't have to worry about forgetting to set it back to nil.

What are your thoughts?


Solution

  • Well, I think you're aiming for something else, but the following code fixes your example and is actually easier to understand:

    transactions.inject(0) { |acum, t| acum += t.amount || 0 }
    

    But I don't don't think the method summing up the amounts should know about the default value for nil amounts, so (even if your question states that I can't argue with it) I would change the amount method to return the default instead:

    def amount
      @amount || 0
    end
    

    Nevertheless I think your example is just too easy to solve and you're actually aiming for answers to a more complex question. Looking forward to all the other answers.