开发者

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 fi开发者_JS百科rst 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?


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.


I don't see where the dynamic scope in your solution is. tap introduces a new lexically scoped block, the values are restored according to the lexical scope.

BTW, the let in Common Lisp doesn't create dynamically scoped variables by itself, either. You have to declare the variables special to make that happen (or it will redefine the value of a variable dynamically if that variable is already special).

EDIT: For the sake of completeness, I quickly implemented a class that implements actual dynamic variable behaviour: http://pastie.org/1700111

The output of that is:

foo
bar
foo

EDIT 2: Here's another implementation that does this for instance variables without the need for a wrapper class: http://pastie.org/1706102


The problem stated can be solved by using the || operator (as shown by rubii).

You can further simplify this by calling the sum method on the Array.

account.transactions.all.sum {|t| t.amount|| 0 }

On the other hand, group calculations should not be done in Ruby. The DB should do all the heavy lifting.

account.transactions.sum(:amount) # SELECT SUM(amount)
                                  # FROM   transactions
                                  # WHERE  account_id = account.id
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜