I had a code looking like this:
def my_function(obj)
if obj.type == 'a'
return [:something]
elsif obj.type == 'b'
return []
elsif obj.type == 'c'
return [obj]
elsif obj.type == 'd'
return [obj]*2
end
end
I want to separate all these if...elsif blocks into functions like this:
def my_function_with_a
return [:something]
end
def my_function_with_b
return []
end
def my_function_with_c(a_parameter)
return [a_parameter]
end
def my_function_with_d(a_parameter)
return [a_parameter] * 2
end
I call these functions with
def my_function(obj)
send(:"my_function_with_#{obj.type}", obj)
end
The problem is that some functions need parameters, others do not. I can easily define def my_function_with_a(nothing=nil), but I'm sure there is a better solution to do this.
#Dogbert had a great idea with arity. I have a solution like this:
def my_function(obj)
my_method = self.method("my_function_with_#{obj.type}")
return (method.arity.zero? ? method.call : method.call(obj))
end
Check how to call methods in Ruby, for that I will recommend you this two resources: wikibooks and enter link description here.
Take a special note on optional arguments where you can define a method like this:
def method(*args)
end
and then you call call it like this:
method
method(arg1)
method(arg1, arg2)
def foo(*args)
[ 'foo' ].push(*args)
end
>> foo
=> [ 'foo' ]
>> foo('bar')
=> [ 'foo', 'bar' ]
>> foo('bar', 'baz')
=> [ 'foo', 'bar', 'baz' ]
def my_function(obj)
method = method("my_function_with_#{obj.type}")
method.call(*[obj].first(method.arity))
end
Change your function to something like:
def my_function_with_foo(bar=nil)
if bar
return ['foo', bar]
else
return ['foo']
end
end
Now the following will both work:
send(:"my_function_with_#{foo_bar}")
=> ['foo']
send(:"my_function_with_#{foo_bar}", "bar")
=> ['foo', 'bar']
You can also write it like this if you don't want to use if/else and you're sure you'll never need nil in the array:
def my_function_with_foo(bar=nil)
return ['foo', bar].compact
end
You can use a default value
def fun(a_param = nil)
if a_param
return ['raboof',a_param]
else
return ['raboof']
end
end
or...
def fun(a_param : nil)
if a_param
return ['raboof',a_param]
else
return ['raboof']
end
end
The latter is useful if you have multiple parameters because now when you call it you can just pass in the ones that matter right now.
fun(a_param:"Hooray")
Related
In my model I have attributes: is_a, is_b and is_c. By default all are null.
I need APIs to set them. These attributes can be set as strictly one or in group. If I am to write APIs, I will be doing following in my model:
def set_as_a # strictly a
self.update_attributes!(:is_a => true, :is_b => false, :is_c => false)
end
def set_as_b # strictly b
self.update_attributes!(:is_a => false, :is_b => true, :is_c => false)
end
... # strictly c
def set_as_a_and_b # a and b
self.update_attributes!(:is_a => true, :is_b => true, :is_c => false)
end
..... # so on
While this works, it does not look elegant. Also if in future if the set has more than 3 attributes, it will result more repetitive code. What is the correct elegant way to achieve this?
class SettableAsABC
ATTRS = [:a, :b, :c]
METHOD_RE = /^set_as_([[:alnum:]]+?(?:_and_[[:alnum:]]+?)*)$/
def method_missing(name, *args)
if name.to_s =~ METHOD_RE
trues = $1.split('_and_').map(&:to_sym)
attrs = Hash[ATTRS.map { |a| ["is_#{a}".to_sym, trues.include?(a)] }]
update_attributes(attrs)
else
super
end
end
def respond_to_missing?(name, include_private = false)
!!(name =~ METHOD_RE) || super
end
end
a = SettableAsABC.new
a.set_as_a_and_c
No defining 2^N methods, just plain Ruby metaprogramming.
EDIT: Good point, #Stefan.
EDIT2: My previous edit introduced a bug. Fixed now.
EDIT3: TIL about respond_to_missing?
I might be misunderstanding something, but why not just write a single method that takes params?:
def set_attributes(opts = {})
update_attributes!(opts) unless opts.none?
end
# usage
set_attributes(is_a: false, is_b: true)
EDIT
To dynamically create methods for combinations of your attributes here is what I came up with:
attributes = %w(a b c d)
(1..attributes.size).flat_map { |size| attributes.combination(size).to_a }.each do |methods|
define_method "set_as_#{methods.join('_and_')}" do
update_attributes!(Hash[methods.map { |v| ["is_#{v}", true] }])
end
end
It will generate the following menthods:
set_as_a
set_as_b
set_as_c
set_as_d
set_as_a_and_b
set_as_a_and_c
set_as_a_and_d
set_as_b_and_c
set_as_b_and_d
set_as_c_and_d
set_as_a_and_b_and_c
set_as_a_and_b_and_d
set_as_a_and_c_and_d
set_as_b_and_c_and_d
set_as_a_and_b_and_c_and_d
How about this?
def set_true(true_fields=[])
attr_hash = {}
true_fields.each { |field| attr_hash[field] = true }
update_attributes(hash)
end
Hope that helps!
I am finding something like below. Constructing a where clause using condition. Is it possible in ruby? or I need to separate it into two where clause?
Post
.where(tag: "A") if condition A
.where(tag: "B") if condition B
.where(user_id: 1)
.order(....)
Actually, my case is like this. Is there any way to handle?
def this_function
#questions = Question.joins(:comment_threads)
.tagged_with(tag_variable, wild: true, any: true) if tag_variable.present?
.where(index_where_clause)
.where("questions.created_at < ?", query_from_date_time)
.order(created_at: :desc).limit(5)
end
def index_where_clause
where_clause = {}
where_clause[:user_detail_id] = current_user_detail.id if params[:type] == "my_question"
where_clause[:comments] = {user_detail_id: current_user_detail.id} if params[:type] == "my_answer"
where_clause[:wine_question_score_id] = params[:wine_question_score_id] if params[:wine_question_score_id].present?
where_clause
end
The methods you're using return relations so you can say things like this:
#questions = Question.joins(:comment_threads)
#questions = #questions.where("questions.created_at < ?", query_from_date_time)
#questions = #questions.tagged_with(tag_variable, wild: true, any: true) if tag_variable.present?
#questions = #questions.where(:user_detail_id => current_user_detail.id) if params[:type] == "my_question"
#questions = #questions.where(:comments => { user_detail_id: current_user_detail.id}) if params[:type] == "my_answer"
#questions = #questions.where(:wine_question_score_id => params[:wine_question_score_id]) if params[:wine_question_score_id].present?
#questions = #questions.order(created_at: :desc).limit(5)
and build the query piece by piece depending on what you have in params.
I'd probably break it down a little more:
def whatever
#questions = Question.joins(:comment_threads)
#questions = #questions.where("questions.created_at < ?", query_from_date_time)
#questions = with_tag(#questions, tag_variable)
#...
#questions = #questions.order(created_at: :desc).limit(5)
end
private
def with_tag(q, tag)
if tag.present?
q.tagged_with(tag, wild: true, any: true)
else
q
end
end
#...
and bury all the noisy bits in little methods to make things cleaner and easier to read. If you're doing this more than once then you could use scopes to hide the noise in the model class and re-use it as needed.
#tap can be helpful for modifying an object in place to apply conditional logic, in this case the object would be your .where conditions:
Post
.where(
{ user_id: 1 }
.tap do |conditions|
conditions[:tag] = 'A' if condition A
conditions[:tag] = 'B' if condition B
end
)
.order(...)
Or, perhaps it's a little cleaner if you create a helper method:
def specific_conditions
{ user_id: 1 }.tap do |conditions|
conditions[:tag] = 'A' if condition A
conditions[:tag] = 'B' if condition B
end
end
Post.where(specific_conditions).order(...)
But as a side note, if there's a case where condition A and condition B can both be true, the second conditions[:tag] = ... line will override the first. If there is not a case where both can be true, you might try to use some kind of collection to look up the proper value for tag.
CONDITION_TAGS = {
a: 'A'.freeze,
b: 'B'.freeze,
}.freeze
def specific_conditions
{ user_id: 1 }
.tap do |conditions|
conditions[:tag] = CONDITION_TAGS[condition_value] if condition_value
end
end
Post.where(specific_conditions).order(...)
#in Question class
scope :with_user_detail, -> (user_detail_id, flag=true) do
where("user_detail_id = ?", user_detail_id) if flag
end
scope :with_user_detail_comments, -> (user_detail_id, flag=true) do
joins(:comment_threads).where("comments.user_detail_id = ?", user_detail_id) if flag
end
scope :with_wine_question_score, -> (wine_question_score_id) do
where("wine_question_score_id = ?", wine_question_score_id) if wine_question_score_id.present?
end
scope :tagged_with_condition, -> (tag_variable, wild, any) do
tagged_with(tag_variable, wild, any) if tag_variable.present?
end
def this_function
my_question_flag = params[:type] == "my_question"
my_answer_flag = params[:type] == "my_answer"
Question.with_user_detail(current_user_detail.id, my_question_flag)
.tagged_with_condition(tag_variable, wild: true, any: true)
.with_user_detail_comments(current_user_detail.id, my_answer_flag)
.with_wine_question_score(params[:wine_question_score_id])
.order(created_at: :desc).limit(5)
end
You can do the following:
condition = {:tag => "A"} if condition A
condition = {:tag => "B"} if condition B
Post
.where(condition)
.where(:user_id => 1)
.order(....)
you have to use scope :
scope :my_scope, -> (variable) { where(some: vatiable) if my_condition }
I'm trying my first foray into metaprogramming and it's not going very well! It's a Rails 4.1 application and I'm trying to refactor an active record model (User) to combine two methods that are very similar. The original methods are slightly complex DB calls and work as expected.
The original code:
def retweet_count(league)
celebrity_ids = Roster.
where("user_id = ? and league_id = ?", self.id, league.id).
select(:celebrity_id).map { |r| r.celebrity_id }
Tweet.where({
tweet_date: league.start_date..league.end_date,
celebrity_id: celebrity_ids
}).select(:retweet_count).inject(0) do |sum, n|
sum + ( n.retweet_count || 0 )
end
end
def favorite_count(league)
celebrity_ids = Roster.
where("user_id = ? and league_id = ?", self.id, league.id).
select(:celebrity_id).map { |r| r.celebrity_id }
Tweet.where({
tweet_date: league.start_date..league.end_date,
celebrity_id: celebrity_ids
}).select(:favorite_count).inject(0) do |sum, n|
sum + ( n.favorite_count || 0 )
end
end
The new code:
twitter_stats_count :retweet, :favorite
private
def twitter_stats_count(*stats)
stats.each do |statistic|
stat = send(statistic).to_s
define_method "#{stat}_count" do |league|
celebrity_ids = Roster.
where("user_id = ? and league_id = ?", self.id, league.id).
select(:celebrity_id).map { |r| r.celebrity_id }
Tweet.where({
tweet_date: league.start_date..league.end_date,
celebrity_id: celebrity_ids
}).select("#{stat}_count").inject(0) do |sum, n|
sum + ( n.send("#{stat}_count") || 0 )
end
end
end
end
The error the new code produces when I try to start my rails server:
/Users/kiddo/.rvm/gems/ruby-2.1.0/gems/activerecord-4.1.0.rc2/lib/active_record/dynamic_matchers.rb:26:in `method_missing': undefined method `twitter_stats_count' for User (call 'User.connection' to establish a connection):Class (NoMethodError)
I can't seem to figure out what I'm doing wrong, so any pointers would be much appreciated!
FYI, here's the final code I got working. I mainly went with Holger Just's suggestions, but incorporated aspects from several others, so upvotes all around!
def team_ids(league)
Roster.where(user_id: self.id, league_id: league.id).pluck(:celebrity_id)
end
def self.twitter_stats_count(*stats)
stats.each do |statistic|
stat = statistic.to_s
define_method "#{stat}_count" do |league|
Tweet.where({
tweet_date: league.start_date..league.end_date,
celebrity_id: self.team_ids(league)
}).sum("#{stat}_count")
end
end
end
twitter_stats_count :retweet, :favorite
There are a couple of issues with your approach:
You call the twitter_stats_count directly on the class, not an instance of the class. As such, the method needs to be a class method. You can define it as a class method with
def self.twitter_stats_count(*stats)
# ...
end
Additionally, you call the method before having it defined. In Ruby, everything (even method definitions) are executed. As such, you can only call methods after they have been defined. Thus, you need to put the call to your twitter_stats_count method after its definition.
That looks quite complicated. If I'm not mistaken, you can reduce the duplication by refactoring your code:
def retweet_count(league)
league_tweets(league).sum(:retweet_count)
end
def favorite_count(league)
league_tweets(league).sum(:favorite_count)
end
def celebrity_ids(league)
Roster.where(user_id: self.id, league_id: league.id).pluck(:celebrity_id)
end
def league_tweets(league)
Tweet.where(
tweet_date: league.start_date..league.end_date,
celebrity_id: celebrity_ids(league)
)
end
twitter_stats_count should be a class method, but what you did is make it a instance method, maybe you can try this:
# no private here
def self.twitter_stats_count(*status)
#your codes here
end
You are getting this error because, you have define twitter_stats_count as a private method, You can't call this on self. You have to put it in a instance method, than call it.
Check this.
For example following gives same error:
class Foo
baz
private
def baz
puts "baz called"
end
end
However this will work:
class Foo
def dummy
baz
end
private
def baz
puts "baz called"
end
end
foo = Foo.new
foo.dummy
After reading the article http://jeffkreeftmeijer.com/2011/method-chaining-and-lazy-evaluation-in-ruby/, I started looking for a better solution for method chaining and lazy evaluation.
I think I've encapsulated the core problem with the five specs below; can anyone get them all passing?
Anything goes: subclassing, delegation, meta-programming, but discouraged for the latter.
It would be favourable to keep dependencies to a minimum:
require 'rspec'
class Foo
# Epic code here
end
describe Foo do
it 'should return an array corresponding to the reverse of the method chain' do
# Why the reverse? So that we're forced to evaluate something
Foo.bar.baz.should == ['baz', 'bar']
Foo.baz.bar.should == ['bar', 'baz']
end
it 'should be able to chain a new method after initial evaluation' do
foobar = Foo.bar
foobar.baz.should == ['baz', 'bar']
foobaz = Foo.baz
foobaz.bar.should == ['bar', 'baz']
end
it 'should not mutate instance data on method calls' do
foobar = Foo.bar
foobar.baz
foobar.baz.should == ['baz', 'bar']
end
it 'should behave as an array as much as possible' do
Foo.bar.baz.map(&:upcase).should == ['BAZ', 'BAR']
Foo.baz.bar.join.should == 'barbaz'
Foo.bar.baz.inject do |acc, str|
acc << acc << str
end.should == 'bazbazbar'
# === There will be cake! ===
# Foo.ancestors.should include Array
# Foo.new.should == []
# Foo.new.methods.should_not include 'method_missing'
end
it "should be a general solution to the problem I'm hoping to solve" do
Foo.bar.baz.quux.rab.zab.xuuq.should == ['xuuq', 'zab', 'rab', 'quux', 'baz', 'bar']
Foo.xuuq.zab.rab.quux.baz.bar.should == ['bar', 'baz', 'quux', 'rab', 'zab', 'xuuq']
foobarbaz = Foo.bar.baz
foobarbazquux = foobarbaz.quux
foobarbazquuxxuuq = foobarbazquux.xuuq
foobarbazquuxzab = foobarbazquux.zab
foobarbaz.should == ['baz', 'bar']
foobarbazquux.should == ['quux', 'baz', 'bar']
foobarbazquuxxuuq.should == ['xuuq', 'quux', 'baz', 'bar']
foobarbazquuxzab.should == ['zab', 'quux', 'baz', 'bar']
end
end
This is inspired by Amadan's answer but uses fewer lines of code:
class Foo < Array
def self.method_missing(message, *args)
new 1, message.to_s
end
def method_missing(message, *args)
dup.unshift message.to_s
end
end
Trivial, isn't it?
class Foo < Array
def self.bar
other = new
other << 'bar'
other
end
def self.baz
other = new
other << 'baz'
other
end
def bar
other = clone
other.unshift 'bar'
other
end
def baz
other = clone
other.unshift 'baz'
other
end
end
The to_s criterion fails because 1.9 has changed the way Array#to_s works. Change to this for compatibility:
Foo.baz.bar.to_s.should == ['bar', 'baz'].to_s
I want cake.
BTW - metaprogramming here would cut down the code size and increase flexibility tremendously:
class Foo < Array
def self.method_missing(message, *args)
other = new
other << message.to_s
other
end
def method_missing(message, *args)
other = clone
other.unshift message.to_s
other
end
end
I use a lot of iterations to define convenience methods in my models, stuff like:
PET_NAMES.each do |pn|
define_method(pn) do
...
...
end
but I've never been able to dynamically define setter methods, ie:
def pet_name=(name)
...
end
using define_method like so:
define_method("pet_name=(name)") do
...
end
Any ideas? Thanks in advance.
Here's a fairly full example of using define_method in a module that you use to extend your class:
module VerboseSetter
def make_verbose_setter(*names)
names.each do |name|
define_method("#{name}=") do |val|
puts "##{name} was set to #{val}"
instance_variable_set("##{name}", val)
end
end
end
end
class Foo
extend VerboseSetter
make_verbose_setter :bar, :quux
end
f = Foo.new
f.bar = 5
f.quux = 10
Output:
#bar was set to 5
#quux was set to 10
You were close, but you don't want to include the argument of the method inside the arguments of your call to define_method. The arguments go in the block you pass to define_method.
Shoertly if you need it inside one class/module:
I use hash but you can put there array of elements etc.
PETS = {
"cat" => "meyow",
"cow" => "moo",
"dog" => "ruff"
}
def do_smth1(v)
...
end
def do_smth(sound,v)
...
end
#getter
PETS.each{ |k,v| define_method(k){ do_smth1(v) } }
#setter
PETS.each{ |k,v| define_method("#{k}="){|sound| do_smth2(sound, v) }