I'm having trouble figuring out this challenge. Here is what I have:
class Dictionary
attr_accessor :entries
def initialize
#x = Hash.new
end
def entries
#x
end
def add(hash)
#x.merge!(hash)
end
end
#d=Dictionary.new
#d.add('fish' => 'aquatic animal')
puts #d.entries
i'm getting => "fishaquatic animal"
I'm WANTING to get => {'fish' => 'aquatic animal'}
to_s on Hash behaves less-than-ideally for some Ruby versions. Try puts #d.entries.inspect.
Update:
The following code works for me (Ruby 1.9.3 and rspec 2.12.0):
class Dictionary
def initialize
#x = Hash.new
end
def entries
#x
end
def add(hash)
#x.merge!(hash)
end
end
describe Dictionary do
before do
#d = Dictionary.new
end
it 'can add whole entries with keyword and definition' do
#d.add('fish' => 'aquatic animal')
#d.entries.should == {'fish' => 'aquatic animal'}
end
end
As written, your code is currently setting #x to a new, empty Hash and then returning it every time you call the entries method.
Try moving that setup code into an initialize method:
class Dictionary
attr_reader :entries
def initialize
#entries = Hash.new
end
def add(hash)
#entries.merge!(hash)
end
end
It looks like to me the rspec codes a bit weird. The second test does an entries method, and the entries method resets the instance variable #x back to blank. So it would better to add the instance variable as an attr_reader and then initialize it when you make a new dictionary object. So it would look something like this
class Dictionary
attr_reader #x
def initialize
#x = Hash.new
end
def add(hash)
#x.merge!(hash)
end
end
and the test would be like this
#d.add(fish: "aquatic animal")
#d.x.should == {'fish' => "aquatic animal"}
Related
I find myself using hash arguments to constructors quite a bit, especially when writing DSLs for configuration or other bits of API that the end user will be exposed to. What I end up doing is something like the following:
class Example
PROPERTIES = [:name, :age]
PROPERTIES.each { |p| attr_reader p }
def initialize(args)
PROPERTIES.each do |p|
self.instance_variable_set "##{p}", args[p] if not args[p].nil?
end
end
end
Is there no more idiomatic way to achieve this? The throw-away constant and the symbol to string conversion seem particularly egregious.
You don't need the constant, but I don't think you can eliminate symbol-to-string:
class Example
attr_reader :name, :age
def initialize args
args.each do |k,v|
instance_variable_set("##{k}", v) unless v.nil?
end
end
end
#=> nil
e1 = Example.new :name => 'foo', :age => 33
#=> #<Example:0x3f9a1c #name="foo", #age=33>
e2 = Example.new :name => 'bar'
#=> #<Example:0x3eb15c #name="bar">
e1.name
#=> "foo"
e1.age
#=> 33
e2.name
#=> "bar"
e2.age
#=> nil
BTW, you might take a look (if you haven't already) at the Struct class generator class, it's somewhat similar to what you are doing, but no hash-type initialization (but I guess it wouldn't be hard to make adequate generator class).
HasProperties
Trying to implement hurikhan's idea, this is what I came to:
module HasProperties
attr_accessor :props
def has_properties *args
#props = args
instance_eval { attr_reader *args }
end
def self.included base
base.extend self
end
def initialize(args)
args.each {|k,v|
instance_variable_set "##{k}", v if self.class.props.member?(k)
} if args.is_a? Hash
end
end
class Example
include HasProperties
has_properties :foo, :bar
# you'll have to call super if you want custom constructor
def initialize args
super
puts 'init example'
end
end
e = Example.new :foo => 'asd', :bar => 23
p e.foo
#=> "asd"
p e.bar
#=> 23
As I'm not that proficient with metaprogramming, I made the answer community wiki so anyone's free to change the implementation.
Struct.hash_initialized
Expanding on Marc-Andre's answer, here is a generic, Struct based method to create hash-initialized classes:
class Struct
def self.hash_initialized *params
klass = Class.new(self.new(*params))
klass.class_eval do
define_method(:initialize) do |h|
super(*h.values_at(*params))
end
end
klass
end
end
# create class and give it a list of properties
MyClass = Struct.hash_initialized :name, :age
# initialize an instance with a hash
m = MyClass.new :name => 'asd', :age => 32
p m
#=>#<struct MyClass name="asd", age=32>
The Struct clas can help you build such a class. The initializer takes the arguments one by one instead of as a hash, but it's easy to convert that:
class Example < Struct.new(:name, :age)
def initialize(h)
super(*h.values_at(:name, :age))
end
end
If you want to remain more generic, you can call values_at(*self.class.members) instead.
There are some useful things in Ruby for doing this kind of thing.
The OpenStruct class will make the values of a has passed to its initialize
method available as attributes on the class.
require 'ostruct'
class InheritanceExample < OpenStruct
end
example1 = InheritanceExample.new(:some => 'thing', :foo => 'bar')
puts example1.some # => thing
puts example1.foo # => bar
The docs are here:
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/ostruct/rdoc/OpenStruct.html
What if you don't want to inherit from OpenStruct (or can't, because you're
already inheriting from something else)? You could delegate all method
calls to an OpenStruct instance with Forwardable.
require 'forwardable'
require 'ostruct'
class DelegationExample
extend Forwardable
def initialize(options = {})
#options = OpenStruct.new(options)
self.class.instance_eval do
def_delegators :#options, *options.keys
end
end
end
example2 = DelegationExample.new(:some => 'thing', :foo => 'bar')
puts example2.some # => thing
puts example2.foo # => bar
Docs for Forwardable are here:
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/forwardable/rdoc/Forwardable.html
Given your hashes would include ActiveSupport::CoreExtensions::Hash::Slice, there is a very nice solution:
class Example
PROPERTIES = [:name, :age]
attr_reader *PROPERTIES #<-- use the star expansion operator here
def initialize(args)
args.slice(PROPERTIES).each {|k,v| #<-- slice comes from ActiveSupport
instance_variable_set "##{k}", v
} if args.is_a? Hash
end
end
I would abstract this to a generic module which you could include and which defines a "has_properties" method to set the properties and do the proper initialization (this is untested, take it as pseudo code):
module HasProperties
def self.has_properties *args
class_eval { attr_reader *args }
end
def self.included base
base.extend InstanceMethods
end
module InstanceMethods
def initialize(args)
args.slice(PROPERTIES).each {|k,v|
instance_variable_set "##{k}", v
} if args.is_a? Hash
end
end
end
My solution is similar to Marc-André Lafortune. The difference is that each value is deleted from the input hash as it is used to assign a member variable. Then the Struct-derived class can perform further processing on whatever may be left in the Hash. For instance, the JobRequest below retains any "extra" arguments from the Hash in an options field.
module Message
def init_from_params(params)
members.each {|m| self[m] ||= params.delete(m)}
end
end
class JobRequest < Struct.new(:url, :file, :id, :command, :created_at, :options)
include Message
# Initialize from a Hash of symbols to values.
def initialize(params)
init_from_params(params)
self.created_at ||= Time.now
self.options = params
end
end
Please take a look at my gem, Valuable:
class PhoneNumber < Valuable
has_value :description
has_value :number
end
class Person < Valuable
has_value :name
has_value :favorite_color, :default => 'red'
has_value :age, :klass => :integer
has_collection :phone_numbers, :klass => PhoneNumber
end
jackson = Person.new(name: 'Michael Jackson', age: '50', phone_numbers: [{description: 'home', number: '800-867-5309'}, {description: 'cell', number: '123-456-7890'})
> jackson.name
=> "Michael Jackson"
> jackson.age
=> 50
> jackson.favorite_color
=> "red"
>> jackson.phone_numbers.first
=> #<PhoneNumber:0x1d5a0 #attributes={:description=>"home", :number=>"800-867-5309"}>
I use it for everything from search classes (EmployeeSearch, TimeEntrySearch) to reporting ( EmployeesWhoDidNotClockOutReport, ExecutiveSummaryReport) to presenters to API endpoints. If you add some ActiveModel bits you can easily hook these classes up to forms for gathering criteria. I hope you find it useful.
For academic reasons, I'd like to make an instance of Ruby class act like a hash.
GOALS
Initialize MyClass instance with a hash # success
Request values from instance of myClass, like a hash # success
Then set properties as a hash # fail
Although some discussion exists, I tried what's out there (1, 2) with no success. Let me know what I'm doing wrong. Thanks!
class MyClass
attr_accessor :my_hash
def initialize(hash={})
#my_hash = hash
end
def [](key)
my_hash[key]
end
def set_prop(key, value)
myhash[key] = value
end
end
test = myClass.new({:a => 3}) #=> #<MyClass:0x007f96ca943898 #my_hash={:a=>3}>
test[:a] #=> 3
test[:b] = 4 #=> NameError: undefined local variable or method `myhash' for #<MyClass:0x007f96ca9d0ef0 #my_hash={:a=>3}>
You declared set_prop, but you're using []= in tests. Did you mean to get this?
class MyClass
attr_accessor :my_hash
def initialize(hash={})
#my_hash = hash
end
def [](key)
my_hash[key]
end
def []=(key, value)
my_hash[key] = value
end
end
test = MyClass.new({:a => 3}) # success
test[:a] # success
test[:b] = 4 # success
test.my_hash # => {:a=>3, :b=>4}
module HashizeModel
def [](key)
sym_key = to_sym_key(key)
self.instance_variable_get(sym_key)
end
def []=(key, value)
sym_key = to_sym_key(key)
self.instance_variable_set(sym_key, value)
end
private
def to_sym_key(key)
if key.is_a? Symbol
return ('#'+key.to_s).to_sym
else
return ('#'+key.to_s.delete('#')).to_sym
end
end
end
You should write it as test = MyClass.new({:a => 3}) and the below code should work.
class MyClass
attr_accessor :my_hash
def initialize(hash={})
#my_hash = hash
end
def [](key)
#my_hash[key]
end
def []=(key,val)
#my_hash[key]=val
end
def set_prop(key, value)
#myhash[key] = value
end
end
test = MyClass.new({:a => 3})
test[:a]
test[:b]= 4
test.my_hash # => {:a=>3, :b=>4}
I have a dictionary class and want to be able to push keys (as keywords) and values (as definitions) into an empty hash with an 'add' method. I don't understand how to syntactically write that. I have included an RSPEC file too.
ruby:
class Dictionary
attr_accessor :keyword, :definition
def entries
#hash = {}
end
def add(options)
options.map do |keyword, definition|
#hash[keyword.to_sym] = definition
end
end
end
Rspec:
require 'dictionary'
describe Dictionary do
before do
#d = Dictionary.new
end
it 'can add whole entries with keyword and definition' do
#d.add('fish' => 'aquatic animal')
#d.entries.should == {'fish' => 'aquatic animal'}
#d.keywords.should == ['fish']
end
Any help is appreciated. Thank you!
UPDATE:
Thank you Dave Newton for replying. I used your code and got this error:
ERROR:
*Failure/Error: #d.keywords.should == ['fish']
NoMethodError:
undefined method `keywords' for #<Dictionary:0x007fb0c31bd458
#hash={"fish"=>"aquatic animal"}>*
I get a different error when I convert 'word' into a symbol using #hash[word.to_sym] = defintion
*Failure/Error: #d.entries.should == {'fish' => 'aquatic animal'}
expected: {"fish"=>"aquatic animal"}
got: {:fish=>"aquatic animal"} (using ==)
Diff:
## -1,2 +1,2 ##
-"fish" => "aquatic animal"
+:fish => "aquatic animal"*
Instantiate your hash in Dictionary's initialize:
class Dictionary
def initialize
#hash = {}
end
def add(defs)
defs.each do |word, definition|
#hash[word] = definition
end
end
end
Right now you have no hash until you call entries, which you don't.
entries should return the existing hash, not create a new one.
keywords should return the hash's keys.
You do not need accessors for keyword and definition. Such singular items are meaningless in a dictionary class. You might want something like lookup(word) that returned a definition.
Also, you convert words to symbols, but I don't know why–particularly since you use string keys in your spec. Pick one, although I'm not convinced this is a case where symbols add value.
Please consider variable naming carefully to provide as much context as possible.
Looking at your Rspec, It looks like you need this setup.
class Dictionary
def initialize
#hash = {}
end
def add(key_value)
key_value.each do |key, value|
#hash[key] = value
end
end
def entries
#hash
end
def keywords
#hash.keys
end
end
Do not use key.to_sym in add method, just key
To give flexibility to add method, I can return the self object to continuesly add.
def add(key_value)
key_value.each do |key, value|
#hash[key] = value
end
self
end
So, I can do this now.
#d.add("a" => "apple").add("b" => "bat")
I want to implement a (class) method attr_accessor_with_client_reset, which does the same thing as attr_accessor, but on every writer it additionally executes
#client = nil
So, for example,
attr_accessor_with_client_reset :foo
should produce the same result as
attr_reader :foo
def foo=(value)
#foo = value
#client = nil
end
How do I achieve this?
Sergio's solution is good, but needlessly complex: there's no need to duplicate the behavior of attr_reader, you can just delegate to it. And there's no need for all this double module include hook hackery. Plus, attr_accessor takes multiple names, so attr_accessor_with_client_reset should, too.
module AttrAccessorWithClientReset
def attr_accessor_with_client_reset(*names)
attr_reader *names
names.each do |name|
define_method :"#{name}=" do |v|
instance_variable_set(:"##{name}", v)
#client = nil
end
end
end
end
class Foo
extend AttrAccessorWithClientReset
attr_reader :client
def initialize
#foo = 0
#client = 'client'
end
attr_accessor_with_client_reset :foo
end
f = Foo.new
f.foo # => 0
f.client # => "client"
f.foo = 1
f.foo # => 1
f.client # => nil
It's actually pretty straightforward if you have some experience in ruby metaprogramming. Take a look:
module Ext
def self.included base
base.extend ClassMethods
end
module ClassMethods
def attr_accessor_with_client_reset name
define_method name do
instance_variable_get "##{name}"
end
define_method "#{name}=" do |v|
instance_variable_set "##{name}", v
#client = nil
end
end
end
end
class Foo
include Ext
attr_reader :client
def initialize
#foo = 0
#client = 'client'
end
attr_accessor_with_client_reset :foo
end
f = Foo.new
f.foo # => 0
f.client # => "client"
f.foo = 1
f.foo # => 1
f.client # => nil
If this code is not completely clear to you, then I strongly recommend this book: Metaprogramming Ruby.
(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