Struct with and without member - ruby

I'm a new Rubyist, currently I'm going to use the Struct in class in order to create a temporary object. However, I encounter a question when I use a member in Struct like this:
class A
attr_accessor :b
B = Struct.new(:name, :print_name) do
def print_name
p "Hello #{name}"
end
end
def initialize(input_name)
#b = B.new(input_name)
end
end
a = A.new('Leo')
a.b.print_name # Hello Leo
But I also receive the same result when my Struct B params don't include :print_name.
B = Struct.new(:name) do
...
end
So what is the different ? And when should I use the member param and when I'm not ?
Thanks

In first case you define a class which's initializer takes two arguments - name and print_name.
In second case you define a class which's initializer takes single argument - name.
These has nothing to do with the fact, that you are defining an instance method called print_name.
So instances of both classes (with and without print_name argument) have print_name method defined, thus both examples work identically.
The difference will be visible when you inspect created objects:
# first case with two arguments
foo = B.new(:a, :b)
foo.inspect
=> "#<struct B name=:a, print_name=:b>"
# second case with single argument
foo = B.new(:a)
foo.inspect
=> "#<struct B name=:a>"
Also, when you'll check instance methods of B class for both cases, you can see the difference:
# first case with two arguments
B.instance_methods false
#=> [:name, :name=, :print_name, :print_name=]
# second case with single argument
B.instance_methods false
#=> [:name, :name=, :print_name]
But I also receive the same result when my Struct B params don't
include :print_name
The difference, is that in first case you are able to do the following:
a.b.print_name = 'new print name'
a.b.inspect
#=> "#<struct B name='Leo', print_name='new print name'>"
Whereas in second case it will fail:
a.b.print_name = 'new print name'
#=> NoMethodError: undefined method 'print_name=' for #<struct B name='Leo'>

Related

Can we call normal constructor and paramatrised constructor at a time in Ruby?

I tried like this. The file name of ClassA is instanceAndClassMethods
class ClassA
def initialize #constructor
puts "This is my constructor"
end
def initialize(a,b)
c=a-b
puts c
end
end
From other class I called above class as both are in same folder like:
require './instanceAndClassMethods' #filename should not contain spaces
obj = ClassA.new #constructor are automatically called when object is created
obj=ClassA.new(33,33)
When I run from command prompt, I'm getting:
Traceback (most recent call last):
2: from callMeth.rb:4:in `<main>'
1: from callMeth.rb:4:in `new'
C:/Users/vkuma102/Desktop/Ruby Learning/instanceAndClassMethods.rb:7:in `initial
ize': wrong number of arguments (given 0, expected 2) (ArgumentError)
If this is the case then it is difficult right whereas we can call both normal constructor and constructor with parameters in Java
No, Ruby does not have method overloading. Unlike e.g. Java or Crystal, you only get one method of the same name per class. Your second def is overwriting the first. It's like writing foo = 7; foo = 19 - the value 7 is not accessible any more from foo.
If you want to distinguish different argument lists, you need to do it yourself. Fortunately, unlike Java, Ruby has optional parameters (i.e. parameters with default values):
class ClassA
def initialize(a=nil, b=nil)
if a && b
c = a - b
puts c
else
puts "This is my constructor"
end
end
end
In addition to the overloading solution, which Amadan suggested, you can also provide factory methods to complement your constructor, for example:
class Foo
def initialize(_a = nil, _b = nil, _c = _nil)
#a, #b, #c = _a, _b, _c
end
# factories
def self.make_fancy_foo(x,y,z)
new(bar(x),y+1,baz(z-y))
end
def self.make_special_foo(x)
new(x,x,x)
end
end
This is how you can use them:
foo1 = Foo.new
foo2 = Foo.new(88)
foo3 = Foo.new(3,6,9)
foo4 = Foo.make_fancy_foo(7,-1,5)
foo5 = Foo.make_special_foo(6)
You're overwriting the first constructor, you can only have one method per name in ruby, to achieve the behavior you want you can do something like:
class ClassA
def initialize(a=nil, b=nil)
a && b ? puts(a+b) : puts "This is my constructor"
end
end
Or:
class ClassA
def initialize(*args)
arg.any? ? deal_with_params : puts "This is my constructor"
end
end

How to define a const on the singleton class?

I want to define a constant for a single instance of an object, not for all instances of an object.
class MyTest
def call
Foo
end
end
t = MyTest.new
t.call # => NameError (as expected)
t.singleton_class.class_eval do
const_set 'Foo', 'foo'
end
t.singleton_class::Foo # => 'foo'
t.call # => NameError
Why does the const lookup not include the const defined in the objects singleton class?
Here is another attempt:
Dog = Class.new { def call; Bark; end }
d = Dog.new
d.call # => NameError (expected)
d.singleton_class.const_set('Bark', 'woof')
d.call # => NameError (not expected)
The constant has been defined in the instance's singleton class but Foo doesn't include (singleton_class)::Foo in one of its possible evaluations so specify it explicitly:
class MyTest
def call
singleton_class::Foo
end
end
I believe it is not possible:
const_set is defined on Module
Ruby looks for constant in nesting and ancestors
There is no place where to define the constant for a MyTest instance
class MyTest
def call
puts (Module.nesting + Module.ancestors).uniq.inspect
end
end
MyTest.new.call
# => [MyTest, Module, Object, Kernel, BasicObject]
I think this comment is correct - t.singleton_class.class_eval first gets the singleton_class of t and then evals something on that singleton classes' class.
Instead what you want is to define the constant on the instance of that singleton class, like so:
t.singleton_class.instance_eval do
Foo = 'foo'
end
After that, t.call returns "foo".
Start be creating an arbitrary set and an instance of that set.
class A; end
a = A.new
#=> #<A:0x00007ff0bbaa9f98>
For convenience, assign a's singleton class1 to a variable.
as = a.singleton_class
#=> #<Class:#<A:0x00007ff0bbaa9f98>>
Create a constant in as whose value equals 3.
as.const_set(:A, 3)
#=> 3
Check that the constant was defined.
as.constants
#=> [:A]
Let's check its value also.
as.const_get(:A)
#=> 3
Now let's create a method in as that references the constant we've created.
as.define_method(:cat) { puts "#{as.const_get(:A)} cats"}
#=> :cat
Try it.
a.cat
#=> "3 cats"
See Module#const_set and Module#const_get.
1. Every Ruby object has a singleton class. Singleton classes are like regular classes except they cannot be subclassed and one cannot create instances of subclasses.

Difference between self.element = 'this' and self.send("element=", 'this')

I am trying to understand why these two things return different values.
Value is a string, and field is a text_field.
def populate_text(field, value)
self.send "user_name=", value
end
# => nil
def populate_text(value)
self.user_name = value
end
# => "value"
Why do self and send have different return values?
This class includes PageObject if that helps.
Ruby's syntax sugar for calling methods whose name ends with = always returns the righthand value, regardless of the return value of the method.
This is not the case when you use send to invoke the method. For example:
class Foo
def bar=(n)
:ohno
end
end
f = Foo.new
x = (f.bar = 42)
y = f.send("bar=", 42)
p [x,y]
#=> [42, :ohno]
So, you would get two different values if your user_name= method has a return value that is not the argument to the method.
Self.Send allows you to dynamically choose your objects, regardless of type.
This lets you data drive your test with very simple code.

How to change the default value of a Struct attribute?

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

Name of the current object/class instance

I need to load a YAML file (I'm experimenting with SettingsLogic) and I'd like the instance to load the YAML with the same name as it. Briefly:
class MySettings < SettingsLogic
source "whatever_the_instance_is_called.yml"
# Do some other stuff here
end
basic_config = MySettings.new # loads & parses basic_config.yml
advanced_cfg = MySettings.new # loads & parses advanced_cfg.yml
...and so on...
The reason for this I don't yet know what configuration files I'll have to load, and typing:
my_config = MySettings.new("my_config.yml")
or
my_config = MySettings.new(:MyConfig)
just seems to be repeating myself.
I took a look around both Google and Stackoverflow, and the closest I came to an answer is either "Get Instance Name" or a discussion about how meaningless an instance name is! (I'm probably getting the query wrong, however.)
I have tried instance#class, and instance#name; I also tried instance#_id2ref(self).
What am I missing?!
Thanks in advance!
O.K., so with local variable assignment, there are snags, such as that assignment might occur slightly later than local variable symbol addition to the local variable list. But here is my module ConstMagicErsatz that I used to implement something similar to out-of-the box Ruby constant magic:
a = Class.new
a.name #=> nil - anonymous
ABC = a # constant magic at work
a.name #=> "ABC"
The advantage here is that you don't have to write ABC = Class.new( name: "ABC" ), name gets assigned 'magically'. This also works with Struct class:
Koko = Struct.new
Koko.name #=> "Koko"
but with no other classes. So here goes my ConstMagicErsatz that allows you to do
class MySettings < SettingsLogic
include ConstMagicErsatz
end
ABC = MySettings.new
ABC.name #=> "ABC"
As well as
a = MySettings.new name: "ABC"
a.name #=> "ABC"
Here it goes:
module ConstMagicErsatz
def self.included receiver
receiver.class_variable_set :##instances, Hash.new
receiver.class_variable_set :##nameless_instances, Array.new
receiver.extend ConstMagicClassMethods
end
# The receiver class will obtain #name pseudo getter method.
def name
self.class.const_magic
name_string = self.class.instances[ self ].to_s
name_string.nil? ? nil : name_string.demodulize
end
# The receiver class will obtain #name setter method
def name= ɴ
self.class.const_magic
self.class.instances[ self ] = ɴ.to_s
end
module ConstMagicClassMethods
# #new method will consume either:
# 1. any parameter named :name or :ɴ from among the named parameters,
# or,
# 2. the first parameter from among the ordered parameters,
# and invoke #new of the receiver class with the remaining arguments.
def new( *args, &block )
oo = args.extract_options!
# consume :name named argument if it was supplied
ɴς = if oo[:name] then oo.delete( :name ).to_s
elsif oo[:ɴ] then oo.delete( :ɴ ).to_s
else nil end
# but do not consume the first ordered argument
# and call #new method of the receiver class with the remaining args:
instance = super *args, oo, &block
# having obtained the instance, attach the name to it
instances.merge!( instance => ɴς )
return instance
end
# The method will search the namespace for constants to which the objects
# of the receiver class, that are so far nameless, are assigned, and name
# them by the first such constant found. The method returns the number of
# remaining nameless instances.
def const_magic
self.nameless_instances =
class_variable_get( :##instances ).select{ |key, val| val.null? }.keys
return 0 if nameless_instances.size == 0
catch :no_nameless_instances do search_namespace_and_subspaces Object end
return nameless_instances.size
end # def const_magic
# ##instances getter and setter for the target class
def instances; const_magic; class_variable_get :##instances end
def instances= val; class_variable_set :##instances, val end
# ##nameless_instances getter for the target class
def nameless_instances; class_variable_get :##nameless_instances end
def nameless_instances= val; class_variable_set :##nameless_instances, val end
private
# Checks all the constants in some module's namespace, recursivy
def search_namespace_and_subspaces( ɱodule, occupied = [] )
occupied << ɱodule.object_id # mark the module "occupied"
# Get all the constants of ɱodule namespace (in reverse - more effic.)
const_symbols = ɱodule.constants( false ).reverse
# check contents of these constant for wanted objects
const_symbols.each do |sym|
# puts "#{ɱodule}::#{sym}" # DEBUG
# get the constant contents
obj = ɱodule.const_get( sym ) rescue nil
# is it a wanted object?
if nameless_instances.map( &:object_id ).include? obj.object_id then
class_variable_get( :##instances )[ obj ] = ɱodule.name + "::#{sym}"
nameless_instances.delete obj
# and stop working in case there are no more unnamed instances
throw :no_nameless_instances if nameless_instances.empty?
end
end
# and recursively descend into the subspaces
const_symbols.each do |sym|
obj = ɱodule.const_get sym rescue nil # get the const value
search_namespace_and_subspaces( obj, occupied ) unless
occupied.include? obj.object_id if obj.kind_of? Module
end
end
end # module ConstMagicClassMethods
end # module ConstMagicErsatz
The above code implements automatic searching of whole Ruby namespace with the aim of finding which constant refers to the given instance, whenever #name method is called.
The only constraint using constants gives you, is that you have to capitalize it. Of course, what you want would be modifying the metaclass of the object after it is already born and assigned to a constant. Since, again, there is no hook, you have to finde the occasion to do this, such as when the new object is first used for its purpose. So, having
ABC = MySettings.new
and then, when the first use of your MySettings instance occurs, before doing anything else, to patch its metaclass:
class MySettings
def do_something_useful
# before doing it
instance_name = self.name
singleton_class.class_exec { source "#{instance_name}.yml" }
end
# do other useful things
end
Shouldn't you be able to do either
File.open(File.join(File.expand_path(File.dir_name(__FILE__)), foo.class), "r")
or
require foo.class
The first one need not be that complicated necessarily. But if I'm understanding you correctly, you can just use foo.class directly in a require or file load statement.
Adjust as necessary for YAML loading, but #class returns a plain old string.
Well if you have tons of variables to instantiate, I'd personally just create a Hash to hold them, it's cleaner this way. Now to instantiate all of this, you could do a loop other all your yaml files :
my_settings = {}
[:basic_config, :advanced_cfg, :some_yaml, :some_yaml2].each do |yaml_to_parse|
my_settings[yaml_to_parse] = MySettings.new(yaml_to_parse)
end
Make sure your initialize method in MySettings deals with the symbol you give it!
Then get your variables like this :
my_settings[:advanced_cfg]
Unfortunately, Ruby has no hooks for variable assignment, but this can be worked around. The strategy outline is as follows: First, you will need to get your MySettings.new method to eval code in the caller's binding. Then, you will find the list of local variable symbols in the caller's binding by calling local_variables method there. Afterwards, you will iterate over them to find which one refers to the instance returned by super call in your custom MySettings.new method. And you will pass its symbol to source method call.

Resources