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
Transaction
s. 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
精彩评论