I have a simple class that, on initialization, takes between one and eight parameters. It sets the accessors to these to use later. Rubocop is trying to arrest me for the ABC being too high, but I'm not sure if there's actually anything wrong with what I've done. Is this a case where I just disable the inspection on my initialize?
class Foo
attr_accessor :one, :two, :three, :four
attr_accessor :five, :six, :seven, :eight
def initialize(p={})
#one = p[:one] if p[:one].present?
# ...
#eight = p[:eight] if p[:eight].present?
end
end
My only thought on reducing size would be to do something like iterating through all my attr_accessors on initialize, seeing if there is a corresponding symbol passed through in the has, and if so assigning it.
class Foo
attr_accessor :one, :two, :three, :four
attr_accessor :five, :six, :seven, :eight
def initialize(p={})
instance_variables.each do |variable|
send("##{variable}") = p["#{send(variable)}".to_sym] if p["#{send(variable)}".to_sym].present?
end
end
end
But this seems kind of weak.
Here is one of the ways to achieve what you are trying to do:
class Foo
attr_accessor(*%i[one two three four five six seven eight])
def initialize(p = {})
p.keys.each { |k| instance_variable_set("##{k}", p.fetch(k, nil)) }
end
end
Check out for Hash#fetch method.
You can also use it to just access the key-value pairs of p variable, if you instead of 8 variables decide to go with one (#p)
EDIT
Just out of curiosity wrote this version (some meta programming used) - it will dynamically add attr_accessor for added instance variables:
class Foo
def initialize(p = {})
p.keys.each do |k|
instance_variable_set("##{k}", p.fetch(k, nil))
self.class.__send__(:attr_accessor, k)
end
end
end
What is happening, is we take provided to initialize method argument (hash p), get its keys and create instance variables from them, assigning each variable with value corresponding to the key. Then we're defining attr_accessor for each of the keys.
a = Foo.new(a: 2, b: 3)
#=> #<Foo:0x00000002d63ad8 #a=2, #b=3>
You shouldn't assign each of those as different variables. You should rather save that to a variable as a single hash, and access the hash when you need the values. In fact, you already seem to have a variable p. So keep that as #p = p.
Related
How can I refactor the following? I have some values stored in my YAML file as nested arrays, but I want to pull all my transactions into two get and set methods. This works, but is obviously limited and bulky. It feels wrong.
module Persistance
#store = YAML::Store.new('store.yml')
def self.get_transaction(key)
#store.transaction { #store[key] }
end
def self.get_nested_transaction(key, sub)
#store.transaction { #store[key][sub] }
end
end
Bonus credit: I also have an additional method for incrementing values in my YAML file. Is there a further way to refactor this code? Does it make sense to just pass blocks to a single database accessing method?
Hey I remember thinking about this when I was practicing PStore a little while ago. I didn't figure out a working approach then but I managed to get one now. By the way, yaml/store is pretty cool and you can take credit for introducing me to it.
Anyway, on with the code. Basically here's a couple important concepts:
The #store is similar to a hash in that you can use [] and []= but it's not actually a hash, it's a YAML::Store.
Ruby 2.3 has a method Hash#dig which is kind of the missing puzzle piece here. You provide a list of keys and it treats each as successive keys. You can use this for both get and set, as my code shows
If #store were a true hash that would be the end of it but's not, so for this answer I added a YAML::Store#dig method which has the same usage as the original.
require 'yaml/store'
class YAML::Store
def dig(*keys)
first_val = self[keys.shift]
if keys.empty?
first_val
else
keys.reduce(first_val) do |result, key|
first_val[key]
end
end
end
end
class YamlStore
attr_reader :store
def initialize filename
#store = YAML::Store.new filename
end
def get *keys
#store.transaction do
#store.dig *keys
end
end
def set *keys, val
#store.transaction do
final_key = keys.pop
hash_to_set = keys.empty? ? #store : #store.dig(*keys)
hash_to_set.send :[]=, final_key, val
end
end
end
filename = 'store.yml'
db = YamlStore.new filename
db.set :a, {}
puts db.get :a
# => {}
db.set :a, :b, 1
puts db.get :a, :b
# => 1
In Ruby, we define class member functions like this
class Dog
def bark
puts "woof"
end
end
What I want to know and have been thoroughly unsuccessful in googling is, how and where does one define methods to act upon object-arrays in Ruby?
I wish to be able to do something like this:
dogs = [Dog.new, Dog.new, Dog.new]
dogs.some_function
How and where do I define some_function?
Note: I am not after solutions to a specific problem, more so the steps in how to define such a function in general.
To make all your dogs bark, you should use each on the array:
dogs.each(&:bark)
which is equivalent to
dogs.each { |dog| dog.bark }
Very rarely you need to define a method on an array, in which case it will be available on all arrays, containing anything. To do that, you need to declare it inside the Array class, by declaring it again:
class Array
def some_function
# do something...
end
end
And then you could run:
dogs = [Dog.new, Dog.new, Dog.new]
dogs.some_function
and also
numbers = [1, 2, 3, 4]
numbers.some_function
You could also create a class that inherits from Array such as
class DogWalker < Array
def some_function
self.each(&:bark)
end
end
class Dog
def bark
puts "woof"
end
end
d = DogWalker.new([Dog.new,Dog.new,Dog.new])
d.some_function
#woof
#woof
#woof
#=> [#<Dog:0x2a5a2e8>, #<Dog:0x2a5a2d0>, #<Dog:0x2a5a2b8>]
This means that you can only call this method on an instance of DogWalker (as well as all the methods available to Array) but does not alter Array itself. Giving you better control of the objects included in it. e.g.
class DogWalker < Array
def initialize(args)
raise ArgumentError.new("Must be an Array of Dogs") unless args.is_a?(Array) && args.all?{|e| e.is_a?(Dog)}
super
end
end
d = DogWalker.new([Dog.new,Dog.new,Dog.new])
#=>[#<Dog:0x2a5a2e8>, #<Dog:0x2a5a2d0>, #<Dog:0x2a5a2b8>]
d = DogWaler.new([Dog.new,12])
#=>ArgumentError: Must be an Array of Dogs
Array.new([1,2,3])
#=>[1,2,3]
As #UriAgassi pointed out very rarely do you need to alter base classes especially if you are designing extended functionality that does not need to/cannot apply across object types. Here is an example:
class Array
def some_function
self.map(&:bark)
end
end
class Dog
def bark
"woof"
end
end
d = [Dog.new,Dog.new]
d.some_function
#=> ["woof","woof"]
d = [1,2,3,4]
d.some_function
#=>NoMethodError: undefined method `bark' for 1:Fixnum
Since Array can accept any object type the Array is fine but the some_function method requires an object that responds_to bark which will raise in the event the object cannot perform the task requested.
In your example 'dogs' is the instance of Array class.
If you want to extend Array class you can add method to it like this:
class Array
def some_function
end
end
According to the documentation unset attributes of Struct are set to nil:
unset parameters default to nil.
Is it possible to specify the default value for particular attributes?
For example, for the following Struct
Struct.new("Person", :name, :happy)
I would like the attribute happy to default to true rather than nil. How can I do this? If I do as follows
Struct.new("Person", :name, :happy = true)
I get
-:1: syntax error, unexpected '=', expecting ')'
Struct.new("Person", :name, :happy = true)
^
-:1: warning: possibly useless use of true in void context
This can also be accomplished by creating your Struct as a subclass, and overriding
initialize with default values as in the following example:
class Person < Struct.new(:name, :happy)
def initialize(name, happy=true); super end
end
On one hand, this method does lead to a little bit of boilerplate; on the other, it does what you're looking for nice and succinctly.
One side-effect (which may be either a benefit or an annoyance depending on your preferences/use case) is that you lose the default Struct behavior of all attributes defaulting to nil -- unless you explicitly set them to be so. In effect, the above example would make name a required parameter unless you declare it as name=nil
Following #rintaun's example you can also do this with keyword arguments in Ruby 2+
A = Struct.new(:a, :b, :c) do
def initialize(a:, b: 2, c: 3); super end
end
A.new
# ArgumentError: missing keyword: a
A.new a: 1
# => #<struct A a=1, b=2, c=3>
A.new a: 1, c: 6
# => #<struct A a=1, b=2, c=6>
UPDATE
The code now needs to be written as follows to work.
A = Struct.new(:a, :b, :c) do
def initialize(a:, b: 2, c: 3)
super(a, b, c)
end
end
I also found this:
Person = Struct.new "Person", :name, :happy do
def initialize(*)
super
self.location ||= true
end
end
#Linuxios gave an answer that overrides member lookup. This has a couple problems: you can't explicitly set a member to nil and there's extra overhead on every member reference. It seems to me you really just want to supply the defaults when initializing a new struct object with partial member values supplied to ::new or ::[].
Here's a module to extend Struct with an additional factory method that lets you describe your desired structure with a hash, where the keys are the member names and the values the defaults to fill in when not supplied at initialization:
# Extend stdlib Struct with a factory method Struct::with_defaults
# to allow StructClasses to be defined so omitted members of new structs
# are initialized to a default instead of nil
module StructWithDefaults
# makes a new StructClass specified by spec hash.
# keys are member names, values are defaults when not supplied to new
#
# examples:
# MyStruct = Struct.with_defaults( a: 1, b: 2, c: 'xyz' )
# MyStruct.new #=> #<struct MyStruct a=1, b=2, c="xyz"
# MyStruct.new(99) #=> #<struct MyStruct a=99, b=2, c="xyz">
# MyStruct[-10, 3.5] #=> #<struct MyStruct a=-10, b=3.5, c="xyz">
def with_defaults(*spec)
new_args = []
new_args << spec.shift if spec.size > 1
spec = spec.first
raise ArgumentError, "expected Hash, got #{spec.class}" unless spec.is_a? Hash
new_args.concat spec.keys
new(*new_args) do
class << self
attr_reader :defaults
end
def initialize(*args)
super
self.class.defaults.drop(args.size).each {|k,v| self[k] = v }
end
end.tap {|s| s.instance_variable_set(:#defaults, spec.dup.freeze) }
end
end
Struct.extend StructWithDefaults
Just add another variation:
class Result < Struct.new(:success, :errors)
def initialize(*)
super
self.errors ||= []
end
end
I think that the override of the #initialize method is the best way, with call to #super(*required_args).
This has an additional advantage of being able to use hash-style arguments. Please see the following complete and compiling example:
Hash-Style Arguments, Default Values, and Ruby Struct
# This example demonstrates how to create Ruby Structs that use
# newer hash-style parameters, as well as the default values for
# some of the parameters, without loosing the benefits of struct's
# implementation of #eql? #hash, #to_s, #inspect, and other
# useful instance methods.
#
# Run this file as follows
#
# > gem install rspec
# > rspec struct_optional_arguments.rb --format documentation
#
class StructWithOptionals < Struct.new(
:encrypted_data,
:cipher_name,
:iv,
:salt,
:version
)
VERSION = '1.0.1'
def initialize(
encrypted_data:,
cipher_name:,
iv: nil,
salt: 'salty',
version: VERSION
)
super(encrypted_data, cipher_name, iv, salt, version)
end
end
require 'rspec'
RSpec.describe StructWithOptionals do
let(:struct) { StructWithOptionals.new(encrypted_data: 'data', cipher_name: 'AES-256-CBC', iv: 'intravenous') }
it 'should be initialized with default values' do
expect(struct.version).to be(StructWithOptionals::VERSION)
end
context 'all fields must be not null' do
%i(encrypted_data cipher_name salt iv version).each do |field|
subject { struct.send(field) }
it field do
expect(subject).to_not be_nil
end
end
end
end
I have a bunch of methods like this in view helper
def background
"#e9eaec"
end
def footer_link_color
"#836448"
end
I'd like these methods exposed to the view, but I'd prefer the helper to be a bit more concise. What's the best way to turn a hash, say, into methods (or something else)?
module MyHelper
{:background => "#e9eaec", :footer_link_color => "#836448"}.each do |k,v|
define_method(k) {v}
end
end
Though I don't think trading this bit of conciseness for the readability of your first approach is necessarily a good idea.
If you want to generalize this, you can add the following method to the Module class:
class Module
def methods_from_hash(hash)
hash.each do |k,v|
define_method(k) {v}
end
end
end
And then in your helper call methods_from_hash(:background => ...).
If you create constants in a namespace, then you can easily whip up accessors for those constants:
class Foo
module Values
FOO = 1
BAR = 'bar'
BAZ = :baz
end
include Values
Values.constants.each do |name|
define_method(name.downcase) do
Values.const_get(name)
end
end
end
foo = Foo.new
p foo.foo # => 1
p foo.bar # => "bar"
p foo.baz # => :baz
The include Values mixes the constants into Foo for the convenience of Foo's own methods. It is not needed for this pattern to work.
Actually, ruby has something called OpenStruct, which is quite awesome and really useful for when you want hash but do not want to use it like one.
require 'ostruct'
colors = OpenStruct.new({:background => "0x00FF00", :color => "0xFF00FF"})
p colors.background #=> "0x00FF00"
Here is my remix of sepp2k's answer. It's a bit more OO and works even in irb. Not sure whether to patch Object or Hash.
class Hash
def keys_to_methods()
each do |k,v|
self.class.send(:define_method, k, Proc.new {v});
end
length
end
end
Test code
hash = {:color_one=>"black", :color_two=>"green"}
hash.keys_to_methods
has.color_one # returns black
OpenStruct: thanks to sepp2k again! I didn't know this existed.
Here is yet another version using method_missing
class Hash
def method_missing(method_id)
key = method_id.id2name
if has_key?(key)
return self[key]
elsif has_key?(key.to_sym)
return self[key.to_sym]
else
super.method_missing(method_id)
end
end
end
hash = {:color_one=>"black", :color_two=>"green"}
hash.color_one
I'm sure I could get the code tighter (if I knew how).
Is there a simple way to list the accessors/readers that have been set in a Ruby Class?
class Test
attr_reader :one, :two
def initialize
# Do something
end
def three
end
end
Test.new
=> [one,two]
What I'm really trying to do is to allow initialize to accept a Hash with any number of attributes in, but only commit the ones that have readers already defined. Something like:
def initialize(opts)
opts.delete_if{|opt,val| not the_list_of_readers.include?(opt)}.each do |opt,val|
eval("##{opt} = \"#{val}\"")
end
end
Any other suggestions?
This is what I use (I call this idiom hash-init).
def initialize(object_attribute_hash = {})
object_attribute_hash.map { |(k, v)| send("#{k}=", v) }
end
If you are on Ruby 1.9 you can do it even cleaner (send allows private methods):
def initialize(object_attribute_hash = {})
object_attribute_hash.map { |(k, v)| public_send("#{k}=", v) }
end
This will raise a NoMethodError if you try to assign to foo and method "foo=" does not exist. If you want to do it clean (assign attrs for which writers exist) you should do a check
def initialize(object_attribute_hash = {})
object_attribute_hash.map do |(k, v)|
writer_m = "#{k}="
send(writer_m, v) if respond_to?(writer_m) }
end
end
however this might lead to situations where you feed your object wrong keys (say from a form) and instead of failing loudly it will just swallow them - painful debugging ahead. So in my book a NoMethodError is a better option (it signifies a contract violation).
If you just want a list of all writers (there is no way to do that for readers) you do
some_object.methods.grep(/\w=$/)
which is "get an array of method names and grep it for entries which end with a single equals sign after a word character".
If you do
eval("##{opt} = \"#{val}\"")
and val comes from a web form - congratulations, you just equipped your app with a wide-open exploit.
You could override attr_reader, attr_writer and attr_accessor to provide some kind of tracking mechanism for your class so you can have better reflection capability such as this.
For example:
class Class
alias_method :attr_reader_without_tracking, :attr_reader
def attr_reader(*names)
attr_readers.concat(names)
attr_reader_without_tracking(*names)
end
def attr_readers
#attr_readers ||= [ ]
end
alias_method :attr_writer_without_tracking, :attr_writer
def attr_writer(*names)
attr_writers.concat(names)
attr_writer_without_tracking(*names)
end
def attr_writers
#attr_writers ||= [ ]
end
alias_method :attr_accessor_without_tracking, :attr_accessor
def attr_accessor(*names)
attr_readers.concat(names)
attr_writers.concat(names)
attr_accessor_without_tracking(*names)
end
end
These can be demonstrated fairly simply:
class Foo
attr_reader :foo, :bar
attr_writer :baz
attr_accessor :foobar
end
puts "Readers: " + Foo.attr_readers.join(', ')
# => Readers: foo, bar, foobar
puts "Writers: " + Foo.attr_writers.join(', ')
# => Writers: baz, foobar
Try something like this:
class Test
attr_accessor :foo, :bar
def initialize(opts = {})
opts.each do |opt, val|
send("#{opt}=", val) if respond_to? "#{opt}="
end
end
end
test = Test.new(:foo => "a", :bar => "b", :baz => "c")
p test.foo # => nil
p test.bar # => nil
p test.baz # => undefined method `baz' for #<Test:0x1001729f0 #bar="b", #foo="a"> (NoMethodError)
This is basically what Rails does when you pass in a params hash to new. It will ignore all parameters it doesn't know about, and it will allow you to set things that aren't necessarily defined by attr_accessor, but still have an appropriate setter.
The only downside is that this really requires that you have a setter defined (versus just the accessor) which may not be what you're looking for.
Accessors are just ordinary methods that happen to access some piece of data. Here's code that will do roughly what you want. It checks if there's a method named for the hash key and sets an accompanying instance variable if so:
def initialize(opts)
opts.each do |opt,val|
instance_variable_set("##{opt}", val.to_s) if respond_to? opt
end
end
Note that this will get tripped up if a key has the same name as a method but that method isn't a simple instance variable access (e.g., {:object_id => 42}). But not all accessors will necessarily be defined by attr_accessor either, so there's not really a better way to tell. I also changed it to use instance_variable_set, which is so much more efficient and secure it's ridiculous.
There's no built-in way to get such a list. The attr_* functions essentially just add methods, create an instance variable, and nothing else. You could write wrappers for them to do what you want, but that might be overkill. Depending on your particular circumstances, you might be able to make use of Object#instance_variable_defined? and Module#public_method_defined?.
Also, avoid using eval when possible:
def initialize(opts)
opts.delete_if{|opt,val| not the_list_of_readers.include?(opt)}.each do |opt,val|
instance_variable_set "##{opt}", val
end
end
You can look to see what methods are defined (with Object#methods), and from those identify the setters (the last character of those is =), but there's no 100% sure way to know that those methods weren't implemented in a non-obvious way that involves different instance variables.
Nevertheless Foo.new.methods.grep(/=$/) will give you a printable list of property setters. Or, since you have a hash already, you can try:
def initialize(opts)
opts.each do |opt,val|
instance_variable_set("##{opt}", val.to_s) if respond_to? "#{opt}="
end
end