What's the idiomatic Ruby way of including a value in an array only if a condition is true?
class ItemPolicy
def initialize(user)
#user = user
#allowed = user.manager?
end
# Suggest improvements to the permitted_attributes method
def permitted_attributes
[:name, :details] + (#allowed ? [:price] : [])
end
end
This doesn't feel very Ruby-ish.
Nothing wrong with it, but I have a feeling that method might grow over time and get a bit more confusing. I'm not sure why #allowed is outside the method, but ignoring that I'd probably do this:
def permitted_attributes
permitted = [:name, :details]
permitted += :price if #allowed
permitted
end
That way you can grow it over time and add other logic, while keeping it readable.
Well, you could do something like this...
#allowed = false
def permitted_attributes
[
:name,
:details,
*(:price if #allowed),
]
end
Honestly that's kind of confusing in my opinion though. Really the best way is probably to just keep it simple:
def permitted_attributes
attrs = [:name, :details]
attrs << :price if #allowed
attrs
end
Like so:
def permitted_attributes
Array[
:name,
:details,
*#allowed ? :price : nil
]
end
or on one line if you prefer:
def permitted_attributes
[:name, :details, *#allowed ? :price : nil]
end
class Array
def add_object_if(object_to_add)
if yield
self << object_to_add
else
self
end
end
end
arr = [1,2]
bool = true
arr.add_object_if(3) { bool }
p arr #=> [1, 2, 3]
bool = false
arr.add_object_if(4) { bool }
p arr #=> [1, 2, 3]
The only thing I could could think of was maybe wrapping the conditional into a small method of its own, which provides a clearer context of why.
Also thought I'd add a attr_reader to remove the reuse of the instance variable.
class ItemPolicy
attr_reader :allowed
def initialize(user)
#user = user
#allowed = user.manager?
end
# Suggest improvements to the permitted_attributes method
def permitted_attributes
[:name, :details] + conditional_attributes
end
def conditional_attributes
return [] unless allowed
[:price]
end
end
Related
TLDR I am working on a project that creates instances using an API. I want to be able to recall all of my instances but can't figure out how. I'm relatively new to Ruby and programming in general so I hope I'm explaining everything well enough. Here's my code.
class Suggestion
attr_accessor :type, :participants
attr_reader :activity, :price, :link, :key, :accessibility
##all = []
def initialize(type, participants)
#type = type
#participants = participants
# #activity = []
# #price = price
# #key = key
# #accessibility = accessibility
##all << self
end
# def save
# ##all << self
# end
def self.all
##all
end
# def events
# ##all.map
# end
def list_events
# binding.pry
Suggestion.all.map #{|event| [:activity, :type, :participants, :price, :link, :key, :accessibility]}
end
end
any and all help would be greatly appreciated
What Already Works
I may be misunderstanding your issue, but it seems like your code mostly works as-is. I refactored it a little to make it a little easier to see what's going on, but your basic approach of self-registration with a class variable during object initialization seems sound. Consider:
class Suggestion
##all = []
def initialize(type, participants)
#type = type
#participants = participants
##all << self
end
def self.all
##all
end
end
s1 = Suggestion.new :foo, %w[Alice Bob]
#=> #<Suggestion:0x00007f9671154578 #participants=["Alice", "Bob"], #type=:foo>
s2 = Suggestion.new :bar, %w[Charlie Dana]
#=> #<Suggestion:0x00007faed7113900 #participants=:bar, #type=:foo>
Suggestion.all
#=>
[#<Suggestion:0x00007f9671154578 #participants=["Alice", "Bob"], #type=:foo>,
#<Suggestion:0x00007f9671089058
#participants=["Charlie", "Dana"],
#type=:bar>]
What Might Need Improvement
You probably just need class or instance methods that "do a thing" with each instance (or selected elements from specific instances) stored in ##all. For example:
# print the object ID of each instance of Suggestion
Suggestion.all.map { |suggestion| suggestion.object_id }
#=> [240, 260]
You might also want to use Array#select to extract specific instances that meet defined criteria, or Array#map to do something with your matching instances. For example:
# re-open the class to add some getter methods
class Suggestion
attr_reader :type, :participants
end
# only operate on instances of a given type
Suggestion.all.select { |s| s.type == :foo }
#=> [#<Suggestion:0x00007f9671154578 #participants=["Alice", "Bob"], #type=:foo>]
# invoke Suggestion#participants on matching instances
Suggestion.all.map { |s| s.participants if s.type == :bar }.compact
#=> [["Charlie", "Dana"]]
As long as you have a collection through which you can iterate (e.g. your class' ##all Array) you can filter, map, or otherwise operate on some or all of your instances at need. There are certainly more complex approaches, but I'd strive to keep it as simple as possible.
Consider the following class definition, where I've made #all a class instance variable, rather than a class variable. Use of the former is generally preferred over use of the latter.
class Suggestion
attr_accessor :type, :participants
#all = []
class << self
attr_accessor :all
end
def initialize(type, participants)
#type = type
#participants = participants
self.class.all << self
end
def list_events
self.class.all.map do |instance|
{ type: instance.type, participants: instance.participants }
end
end
def self.list_events
all.map do |instance|
{ type: instance.type, participants: instance.participants }
end
end
end
The snippet
class << self
attr_accessor :all
end
creates a read-write accessor for #all. class << self causes self to be changed to Suggestion's singleton class. If desired you could replace this with
superclass.public_send(:attr_accessor, :all)
Let's try it.
Suggestion.all
#=> []
s1 = Suggestion.new(1, ['Bob', 'Sally'])
#=> #<Suggestion:0x00007fc677084888 #type=1, #participants=["Bob", "Sally"]>
Suggestion.all
#=> [#<Suggestion:0x00007fc677084888 #type=1, #participants=["Bob", "Sally"]>]
s2 = Suggestion.new(2, ['Ronda', 'Cliff'])
#<Suggestion:0x00007fc677b577b0 #type=2, #participants=["Ronda", "Cliff"]>
Suggestion.all
#=> [#<Suggestion:0x00007fc677084888 #type=1, #participants=["Bob", "Sally"]>,
# #<Suggestion:0x00007fc677b577b0 #type=2, #participants=["Ronda", "Cliff"]>]
s3 = Suggestion.new(3, ['Weasel', 'Isobel'])
#=> #<Suggestion:0x00007fc677077598 #type=3, #participants=["Weasel", "Isobel"]>
Suggestion.all
#=> [#<Suggestion:0x00007fc677084888 #type=1, #participants=["Bob", "Sally"]>,
# #<Suggestion:0x00007fc677b577b0 #type=2, #participants=["Ronda", "Cliff"]>,
# #<Suggestion:0x00007fc677077598 #type=3, #participants=["Weasel", "Isobel"]>]
s1.list_events
#=> [{:type=>1, :participants=>["Bob", "Sally"]},
# {:type=>2, :participants=>["Ronda", "Cliff"]},
# {:type=>3, :participants=>["Weasel", "Isobel"]}]
Suggestion.list_events
#=> [{:type=>1, :participants=>["Bob", "Sally"]},
# {:type=>2, :participants=>["Ronda", "Cliff"]},
# {:type=>3, :participants=>["Weasel", "Isobel"]}]
I included Suggestion#instance to show how an instance can access (read or write) the class instance variable #all.
I was wondering if something like this was possible?
info = arrange_info({|x| [x.name, x.number]}, info_array)
def arrange_info(block, info)
info.map(block).to_h
end
This would allow me to pass different blocks to arrange the array is different ways, how I have it now doesn't work, but is something like this possible?
A block can be passed as a method argument, but it needs to be the last one. You also cannot call a method before it has been defined :
def arrange_info(info, &block)
info.map(&block).to_h
end
info = arrange_info(info_array){|x| [x.name, x.number]}
Here's a small test :
class X
def initialize(name, number)
#name = name
#number = number
end
attr_reader :name, :number
end
def arrange_info(info, &block)
info.map(&block).to_h
end
info_array = [X.new('a', 1), X.new('b', 2)]
p info = arrange_info(info_array) { |x| [x.name, x.number] }
#=> {"a"=>1, "b"=>2}
Adding to Eric's answers.
These are equivalent
def arrange_info(info, &block)
info.map(&block).to_h
end
def arrange_info(info) # takes a block
info.map { |each| yield each }.to_h
end
The latter avoids materializing the block as an object.
Lets say that I have the class:
class Car
attr_accessor :brand, :color
def initialize(br, col)
#brand = br
#color = col
end
end
first_car = Car.new 'Mercedes', 'Yellow'
second_car = Car.new 'Jaguar', 'Orange'
third_car = Car.new 'Bentley', 'Pink'
array_of_cars = [first_car, second_car]
So, the idea is to define a method to_proc in the class Array so this can happen:
array_of_cars.map(&[:brand, :color]) #=> [['Mercedes', 'Yellow'], ['Jaguar', 'Orange'], ['Bentley', 'Pink']]
or how could I somehow do something like this:
[:brand, :color].to_proc.call(first_car) #=> ['Mercedes', 'Yellow']
The standard Symbol#to_proc is, more or less, a e.send(self) call and you just want to send an array full of symbols to each element so just say exactly that with something like this:
class Array
def to_proc
proc { |e| self.map { |m| e.send(m) } }
end
end
I'd probably not patch Array for this, I'd just use a local lambda if I wanted something easier to read:
brand_and_color = lambda { |e| [:brand, :color].map { |s| e.send(s) } }
array_of_cars.map(&brand_and_color)
class Array
def to_proc(arr)
self.map do |elem|
arr.map { |attr| elem.send(attr) }
end
end
end
Like mu is too short mentioned, I wouldn't patch Array like this.
But, this will get the work done for you.
Invoke it as such:
array_of_cars.to_proc([:brand, :color])
Maybe use a normal method?
module ConvertInstanceToHash
def to_proc(obj, keys)
hash = {}
obj.instance_variables.each {|var| hash[var.to_s.delete("#")] = obj.instance_variable_get(var)}
hash = hash.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
hash.values_at(*keys).compact
end
end
irb(main):009:0> first_car = Car.new 'Mercedes', 'Yellow'
=> #<Car:0x000000045fb870 #brand="Mercedes", #color="Yellow">
irb(main):010:0> to_proc(first_car, [:brand, :color])
=> ["Mercedes", "Yellow"]
Considering this simple code :
class Yeah
attr_reader :foo
attr_reader :fool
attr_reader :feel
def initialize(foo: "test", fool: {}, feel: [])
#foo = foo
#fool = fool
end
end
test = Yeah::new
pp test
test.fool[:one] = 10
pp test
Output :
#<Yeah:0x000008019a84a0 #foo="test", #fool={}>
#<Yeah:0x000008019a84a0 #foo="test", #fool={:one=>10}>
My Question is, there is a "simple","clean" way, to do read accesors to real readonly Array,Hash attributs or I need to inherit Array or Hash with a lot of locking hard to write, (undef,alias) or using Proxy, delegate or others patterns like this ?
You can think of something like below :
class Yeah
def self.reader_meth
%i(foo fool feel).each do |m|
define_method(m){instance_variable_get("##{m}").dup.freeze}
end
end
def initialize(foo: "test", fool: {}, feel: [])
#foo = foo
#fool = fool
#feel =feel
end
reader_meth
end
test = Yeah.new
test # => #<Yeah:0x8975498 #foo="test", #fool={}, #feel=[]>
test.fool[:one] = 10 # can't modify frozen Hash (RuntimeError)
test # => #<Yeah:0x8975498 #foo="test", #fool={}, #feel=[]>
Because i want to generalize this solution and to prevent "evil" evals :
i finally, from the Arup solution arrived to this :
class Module
def attr_readonly *syms
syms.each do |method|
define_method(method){
return self.instance_variable_get("##{method.to_s}").dup.freeze
}
end
end
end
class Yeah
attr_reader :foo
attr_readonly :fool
attr_reader :feel
def initialize(foo: "test", fool: {}, feel: [])
#foo = foo
#fool = fool
#feel = feel
end
end
What about Object#freeze:
class Yeah
def fool
#fool.freeze
end
def initialize(fool={})
#fool = fool
end
end
(Big edit, I got part of the way there…)
I've been hacking away and I've come up with this as a way to specify things that need to be done before attributes are read:
class Class
def attr_reader(*params)
if block_given?
params.each do |sym|
define_method(sym) do
yield
self.instance_variable_get("##{sym}")
end
end
else
params.each do |sym|
attr sym
end
end
end
end
class Test
attr_reader :normal
attr_reader(:jp,:nope) { changethings if #nope.nil? }
def initialize
#normal = "Normal"
#jp = "JP"
#done = false
end
def changethings
p "doing"
#jp = "Haha!"
#nope = "poop"
end
end
j = Test.new
p j.normal
p j.jp
But changethings isn't being recognised as a method — anyone got any ideas?
You need to evaluate the block in the context of the instance. yield by default will evaluate it in its native context.
class Class
def attr_reader(*params, &blk)
if block_given?
params.each do |sym|
define_method(sym) do
self.instance_eval(&blk)
self.instance_variable_get("##{sym}")
end
end
else
params.each do |sym|
attr sym
end
end
end
end
Here's another alternative approach you can look at. It's not as elegant as what you're trying to do using define_method but it's maybe worth looking at.
Add a new method lazy_attr_reader to Class
class Class
def lazy_attr_reader(*vars)
options = vars.last.is_a?(::Hash) ? vars.pop : {}
# get the name of the method that will populate the attribute from options
# default to 'get_things'
init_method = options[:via] || 'get_things'
vars.each do |var|
class_eval("def #{var}; #{init_method} if !defined? ##{var}; ##{var}; end")
end
end
end
Then use it like this:
class Test
lazy_attr_reader :name, :via => "name_loader"
def name_loader
#name = "Bob"
end
end
In action:
irb(main):145:0> t = Test.new
=> #<Test:0x2d6291c>
irb(main):146:0> t.name
=> "Bob"
IMHO changing the context of the block is pretty counter-intuitive, from a perspective of someone who would use such attr_reader on steroids.
Perhaps you should consider plain ol' "specify method name using optional arguments" approach:
def lazy_attr_reader(*args, params)
args.each do |e|
define_method(e) do
send(params[:init]) if params[:init] && !instance_variable_get("##{e}")
instance_variable_get("##{e}")
end
end
end
class Foo
lazy_attr_reader :foo, :bar, :init => :load
def load
#foo = 'foo'
#bar = 'bar'
end
end
f = Foo.new
puts f.bar
#=> bar