Reputation: 563
Since a ||= 1
is equivalent to a || a = 1
, one can state that this is synctactic sugar for this:
if a.nil?
a = 1
end
Likewise, given that session
is a hash-like object, the following:
def increment_session_counter
session[:counter] ||= 0
session[:counter] += 1
end
is equivalent to:
def increment_session_counter
if session[:counter].nil?
session[:counter] = 0
end
session[:counter] += 1
end
Does that mean that the implicit if
statement will be executed every time in the original definition of increment_session_counter
? Since session[:counter]
will mostly likely be nil
only the first time (i.e. << 1% of the time), I have the feeling that the following code is better, since the implicit if
won't be fired every time:
def increment_session_counter
session[:counter] += 1
rescue NoMethodError
session[:counter] = 1
end
Is this code better in such sense?
Having said that, I have no idea how Rescue
is implemented in ruby and whether it's actually relevant regarding the tiny little optimization that can bring if so.
Upvotes: 5
Views: 694
Reputation: 563
I tried some benchmarks with the following code. This is a rails app. Rails v.5.1.1 / Ruby v2.4.1
Originally the purpose is just to count the number of visits in any show
actions of some controllers like so:
include SessionCount
...
def show
...
@counter = increment_session_counter
...
Then I could display the counter in the relevant views.
So I made a concern with the appropriate code with both version of the default assignment I wanted to test:
module SessionCount
private
#Counter version A
def increment_session_counter_A
session[:counter] ||= 0
session[:counter] += 1
end
#Counter version B
def increment_session_counter_B
session[:counter] += 1
rescue
session[:counter] = 1
end
end
In order to test both versions of default assignment I changed my controllers code as following:
include SessionCount
...
def show
...
t0 = Time.now
1000000.times do
session[:counter] = 0; #initialization for normalization purpose
increment_session_counter_A
end
t1 = Time.now
puts "Elapsed time: #{((end_time - beginning_time)*1000).round} ms"
@counter = increment_session_counter_A
...
Remark: In that code the initialization is here in order to enforce the "happy path" (where the value is not nil). In real scenario this will occurs only the first time for a given user.
Here are the results:
I get an average of 3100 ms with version A (||=
operator).
I get an average of 2000 ms with version B (rescue
).
But the interesting part begins now..
In the previous code the code is executed following the "happy path" where no exception occurs..
So I changed the initialization I made for normalization purpose as following in order to enforce the "exception path":
1000000.times do
session[:counter] = nil; #initialization for normalization purpose
increment_session_counter_A
end
And here is the results:
I get an average of 3500 ms with version A (||=
operator).
I get an average of around 60 000 ms with version B (rescue
). Yes I tried a couple of times only..
So here I can conclude as spickermann said that exception handling is indeed quite expensive.
But I think that there are many cases where the first time initialization happen very rarely (like a blog where there is no post at the very beginning)..
In such situations there is no reason to test for nil
every time and it could be interesting to use the exception handling.
Am I wrong?
I don't want to nitpick about some ms.. Here I just want to know if this idiom makes sense as a design pattern. I see the difference of both of these versions like the difference between while... end
and do ... while
since the intent is not the same.
Upvotes: 1
Reputation: 114218
session[:counter] += 1
does three things:
Hash#[]
)Integer#+
)Hash#[]=
)That's pretty convenient, but its brevity also makes it inflexible.
Providing a default is much easier if you separate the steps:
def increment_session_counter
session[:counter] = session.fetch(:counter, 0) + 1
end
Upvotes: 3
Reputation: 1607
Catching the error is a pretty clever idea, but it's also harder to read than using ||=
. But even easier would be to set the initial value when you create the hash in the first place:
@session = {:counter => 0}
def increment_session_counter
@session[:counter] += 1
end
That doesn't work when the key isn't known beforehand:
def increment_user_counter(username)
@session[username] ||= 0
@session[username] += 1
end
However, in that case your assumption that the value of the counter is only nil once is thrown into jeopardy. In fact, since many distributions follow the power law, it's likely 1 will be the most common count.
Therefore, if you know the possible values beforehand, it's best to set them to zero when the program or class is initialized and skip the need for the default value check. And if you don't know all possible values upfront, you will likely find the ||=
statement is needed as often as not. There are probably few scenarios where using an exception is the best solution assuming checking for nil
is substantially cheaper.
Upvotes: 3