I'm trying to implement a funky version of method chaining. Returning the instance of the class after each function call is easy, you just do
def chainable_method
some_code()
self
end
My idea is that the methods you can call depend on the previous method call. I'm trying to achieve this by returning an object belonging to the containing object. The contained object will have a few special methods, and then implement method_missing to return the containing object's instance.
Edit: The child object has some state associated with it that should be in itself, and not the parent. It might not have been clear previously as to why I need a whole instance for just method calls.
super is irrelevant in this case because the contained object doesn't inherit from the containing object, and I wouldn't want to call the containing object's methods on the contained object anyway - I want to call the containing object's methods on the containing object itself. I want the containing object, not the containing object class.
Not sure if this is possible.
Edit: reworded everything to use "containing/contained object" instead of the completely incorrect parent/child object.
Also, I'm using 1.9.3, if that matters. Version isn't important, I can change if needed.
My explanation was probably unclear. Here's the code:
class AliasableString
def initialize(string)
#string = string
end
def as(aka)
#aka = aka
end
def has_aka?
!#aka.nil?
end
# alias is a reserved word
def aka
#aka
end
def to_s
#string + (self.has_aka? ? (" as " + #aka) : "")
end
end
class Query
def initialize
#select_statements = Array.new
end
def select(statement)
select_statement = AliasableString.new(statement)
#select_statements.push(select_statement)
select_statement
end
def print
if #select_statements.size != 0
puts "select"
#select_statements.each_with_index {| select, i|
puts select
}
end
end
end
# Example usage
q0 = Query.new
q0.select("This is a select statement")
.select("Here's another one")
.as("But this one has an alias")
.select("This should be passed on to the parent!")
q0.print
I haven't yet fully implemented print. AliasableString needs to have #string and #aka separate so I can pull them apart later.
First of all, it doesn't matter what class of object is contained within a Query instance. All of the syntax shown on your 'example usage' section is appropriately defined in Query. The only requirement of the objects contained within a query instance is that they respond to as (or some similar method). What you have here is something like a state machine, but the only state that really matters is that some object occupies the last position in the select_statements array. Here's how I would build this (again, based mostly on your example at the end, I'm afraid I can't quite follow your initial explanation):
class Query
# ... initialize, etc.
def select(statement, statement_class = AliasableString)
select_statements << statement_class.new(statement)
self
end
def as(aka)
# this will only ever be used on the most recent statement added
statement_to_alias = select_statements.last
# throw an error if select_statements is empty (i.e., :last returns nil)
raise 'You must add a statement first' unless statement_to_alias
# forward the message on to the statement
statement_to_alias.as(aka)
# return the query object again to permit further chaining
self
end
end
AliasableString doesn't need to know a thing about Query; all it needs to do is respond appropriately to as.
Related
Is there were a way to make an #property into a method with set and get, so, #property would call a method instead of returning an actual property, and #property = someval would also call a method instead of assigning to an actual property?
In my project, objects store values in a database. Consider this simple database module that stores records in memory. In my real life project it's a DBM like PostgreSQL:
module MyDB
RECORDS = {}
def self.create(pk)
RECORDS[pk] ||= {}
end
def self.set(pk, key, val)
return RECORDS[pk][key] = val
end
def self.get(pk, key)
return RECORDS[pk][key]
end
end
Objects have fields that are stored in that database. So, in this class, the species field is stored in and retrieved from the database:
class Pet
def initialize(pk)
#pk = pk
MyDB.create(#pk)
end
def species=(val)
MyDB.set #pk, 'breed', val
end
def species()
return MyDB.get(#pk, 'breed')
end
end
A simple use of the Pet class could look like this:
motley = Pet.new('motley')
motley.species = 'cat'
It works currently, but here's where I ran into an annoyance. I did something like this within the class:
def some_method(newval)
#species = newval
end
Then, when I ran the code I got this result:
motley.some_method 'whatever'
puts motley.species #=> cat
Then I realize that wasn't corrent and what I should have done is:
def some_method(newval)
self.species = newval
end
I think #species = newval makes sense. It feels like I'm setting a property of the object.
Is were a way to assign a method to the property, something like:
def :#species=(val)
return MyDB.set(#pk, 'breed', 'val')
end
def :#species
return MyDB.get(#pk, 'breed')
end
Is there a way to do such a thing? Should there be?
Is there a way to do such a thing?
No. In Ruby setter and getter methods are the way to get/set the internal state of an object. Instance variables are just lexical variables that are scoped to an instance.
Ruby is a language based on message passing and #foo = bar sends the message =, bar to the recipient that is the lexical variable #foo. If it called self##foo= instead that would break the entire model of the language.
Should there be?
Hell no.
Do we really need a completely new language feature just because you find it hard to remember to call self.foo= instead of #foo =? No.
Would this feature add anything to the language that cannot already be done? No.
Would it break existing code? Yes.
Saw this on another page:
"with getter, one obtains the current value of #a, without modifying it."
"with setter, one modifies #a, and get its new value as return value."
However, looking at this code from the cancan wiki, I see that both the setter and the getter are actually doing something to the variable in it.
def roles=(roles)
self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.inject(0, :+)
end
def roles
ROLES.reject do |r|
((roles_mask || 0) & 2**ROLES.index(r)).zero?
end
end
It looks like the getter is actually returning a truth value, and if not that, at least some sort of transformation. So is this "getters get without modifying, setters set with modifications" rule actually true?
That is the wrong way to think about "getters" and "setters". Instead, think this:
A setter alters the state of an object. It might set a simple instance variable. It might set several instances variables. As in the posted code it might transform the information before saving it.
A getter retrieves some information about the state of the object. It doesn't matter what this is; it could be a value directly stored in an instance variable. Or it could be some other value based upon the current state of the object, as in the post.
It is usually advisable for the getter and setter to take/return the same type of value and affect/report the object state in a consistent way. In the above the exposed type is an "Array of Roles" and it represents the Roles associated with the object.
Using extra named methods can show the intent of the posted code more clearly as the complex bit mask building/consuming expressions can be extracted out; note the symmetry:
def toMaskFromArray (roles)
(roles & ROLES).map { |r| 2**ROLES.index(r) }.inject(0, :+)
end
def toArrayFromMask (mask)
ROLES.reject do |r|
((mask || 0) & 2**ROLES.index(r)).zero?
end
end
def roles=(roles)
self.roles_mask = toMaskFromArray(roles)
end
def roles
toArrayFromMask(self.roles_mask)
end
This question has been asked before: Read and write YAML files without destroying anchors and aliases?
I was wondering how to solve that problem with many anchors and aliases?
thanks
The problem here is that anchors and aliases in Yaml are a serialization detail, and so aren’t part of the data after it’s been parsed, so the original anchor name isn’t known when writing the data back out to Yaml. In order to keep the anchor names when round tripping you need to store them somewhere when parsing so that they are available later when serializing. In Ruby any object can have instance variables associated with it, so an easy way to achieve this would be to store the anchor name in an instance variable of the objet in question.
Continuing from the example in the earlier question, for hashes we can change our redifined revive_hash method so that if the hash is an anchor then as well as recording the anchor name in the #st variable so later alises can be recognised, we add the it as an instance variable on the hash.
class ToRubyNoMerge < Psych::Visitors::ToRuby
def revive_hash hash, o
if o.anchor
#st[o.anchor] = hash
hash.instance_variable_set "#_yaml_anchor_name", o.anchor
end
o.children.each_slice(2) { |k,v|
key = accept(k)
hash[key] = accept(v)
}
hash
end
end
Note that this only affects yaml mappings that are anchors. If you want to have other types to keep their anchor name you’ll need to look at psych/visitors/to_ruby.rb and make sure the name is added in all cases. Most types can be included by overriding register but there are a couple of others; search for #st.
Now that the hash has the desired anchor name associated with it, you need to make Psych use it instead of the object id when serializing it. This can be done by subclassing YAMLTree. When YAMLTree processes an object, it first checks to see if that object has been seen already, and emits an alias for it if it has. For any new objects, it records that it has seen the object in case it needs to create an alias later. The object_id is used as the key in this, so you need to override those two methods to check for the instance variable, and use that instead if it exists:
class MyYAMLTree < Psych::Visitors::YAMLTree
# check to see if this object has been seen before
def accept target
if anchor_name = target.instance_variable_get('#_yaml_anchor_name')
if #st.key? anchor_name
oid = anchor_name
node = #st[oid]
anchor = oid.to_s
node.anchor = anchor
return #emitter.alias anchor
end
end
# accept is a pretty big method, call super to avoid copying
# it all here. super will handle the cases when it's an object
# that's been seen but doesn't have '#_yaml_anchor_name' set
super
end
# record object for future, using '#_yaml_anchor_name' rather
# than object_id if it exists
def register target, yaml_obj
anchor_name = target.instance_variable_get('#_yaml_anchor_name') || target.object_id
#st[anchor_name] = yaml_obj
yaml_obj
end
end
Now you can use it like this (unlike the previous question, you don’t need to create a custom emitter in this case):
builder = MyYAMLTree.new
builder << data
tree = builder.tree
puts tree.yaml # returns a string
# alternativelty write direct to file:
File.open('a_file.yml', 'r+') do |f|
tree.yaml f
end
here's a slightly modified version for up to newer versions of the psych gem. before it gave me the following error:
NoMethodError - undefined method `[]=' for #<Psych::Visitors::YAMLTree::Registrar:0x007fa0db6ba4d0>
the register method moved into a subclass of YAMLTree, so this works now with respect to everything what matt says in his answer:
class ToRubyNoMerge < Psych::Visitors::ToRuby
def revive_hash hash, o
if o.anchor
#st[o.anchor] = hash
hash.instance_variable_set "#_yaml_anchor_name", o.anchor
end
o.children.each_slice(2) { |k,v|
key = accept(k)
hash[key] = accept(v)
}
hash
end
end
class MyYAMLTree < Psych::Visitors::YAMLTree
class Registrar
# record object for future, using '#_yaml_anchor_name' rather
# than object_id if it exists
def register target, node
anchor_name = target.instance_variable_get('#_yaml_anchor_name') || target.object_id
#obj_to_node[anchor_name] = node
end
end
# check to see if this object has been seen before
def accept target
if anchor_name = target.instance_variable_get('#_yaml_anchor_name')
if #st.key? anchor_name
oid = anchor_name
node = #st[oid]
anchor = oid.to_s
node.anchor = anchor
return #emitter.alias anchor
end
end
# accept is a pretty big method, call super to avoid copying
# it all here. super will handle the cases when it's an object
# that's been seen but doesn't have '#_yaml_anchor_name' set
super
end
end
I had to further modify the code that #markus posted to work with Psych v2.0.17.
Here's what I ended up with. I hope it helps someone else save quite a bit of time. :-)
class ToRubyNoMerge < Psych::Visitors::ToRuby
def revive_hash hash, o
if o.anchor
#st[o.anchor] = hash
hash.instance_variable_set "#_yaml_anchor_name", o.anchor
end
o.children.each_slice(2) do |k,v|
key = accept(k)
hash[key] = accept(v)
end
hash
end
end
class Psych::Visitors::YAMLTree::Registrar
# record object for future, using '#_yaml_anchor_name' rather
# than object_id if it exists
def register target, node
#targets << target
#obj_to_node[_anchor_name(target)] = node
end
def key? target
#obj_to_node.key? _anchor_name(target)
rescue NoMethodError
false
end
def node_for target
#obj_to_node[_anchor_name(target)]
end
private
def _anchor_name(target)
target.instance_variable_get('#_yaml_anchor_name') || target.object_id
end
end
class MyYAMLTree < Psych::Visitors::YAMLTree
# check to see if this object has been seen before
def accept target
if anchor_name = target.instance_variable_get('#_yaml_anchor_name')
if #st.key? target
node = #st.node_for target
node.anchor = anchor_name
return #emitter.alias anchor_name
end
end
# accept is a pretty big method, call super to avoid copying
# it all here. super will handle the cases when it's an object
# that's been seen but doesn't have '#_yaml_anchor_name' set
super
end
def visit_String o
if o == '<<'
style = Psych::Nodes::Scalar::PLAIN
tag = 'tag:yaml.org,2002:str'
plain = true
quote = false
return #emitter.scalar o, nil, tag, plain, quote, style
end
# visit_String is a pretty big method, call super to avoid copying it all
# here. super will handle the cases when it's a string other than '<<'
super
end
end
In an Array subclass (just an array that does some coercing of input values) I've defined #concat to ensure values are coerced. Since nobody ever uses #concat and is more likely to use #+= I tried to alias #+= to #concat, but it never seems to get invoked. Any ideas?
Note that the coercing is actually always to objects of a particular superclass (which accepts input via the constructor), in case this code seems not to do what I describe. It's part of an internal, private API.
class CoercedArray < Array
def initialize(type)
super()
#type = type
end
def push(object)
object = #type.new(object) unless object.kind_of?(#type)
super
end
def <<(object)
push(object)
end
def concat(other)
raise ArgumentError, "Cannot append #{other.class} to #{self.class}<#{#type}>" unless other.kind_of?(Array)
super(other.inject(CoercedArray.new(#type)) { |ary, v| ary.push(v) })
end
alias :"+=" :concat
end
#concat is working correctly, but #+= seems to be completely by-passed.
Since a += b is syntactic sugar for a = a + b I'd try to overwrite the + method.
- EDIT, SOLVED -
Ended up creating a method Object#rec to accomplish what I needed, this is the result:
#$l stores the last object on which Object#rec was called
$l=nil;class Object;def rec;$l=self;end;end
class String
attr_accessor :ref
alias_method :old_reverse, :reverse
def reverse
self.rec.old_reverse
end
def my_method
$l.ref + ' ' + self
end
end
a = "Hello"
b = "dlroW" ; b.ref = a
p b.reverse.my_method #=> Hello World
If anyone has a better way the question is still open.
- EDIT, SOLVED -
The problem:
I have a situation similar to this:
obj.method1.method2
where method1 returns something other than obj and I need method2 to access obj again as it holds a reference I need.
For example:
class String
attr_accessor :ref
def my_method(b)
b.ref + ' ' + self
end
end
a = "Hello"
b = "dlroW" ; b.ref = a
#I want my_method to access 'b.ref' without having to pass 'b'
p b.reverse.my_method(b) #=> Hello World
Alternative:
I know I could avoid having to pass b again if I used obj.my_method and my_method did both reversing(for the example) and accessing the reference, or like commented by the Tin Man have method1 change obj but return the original obj, but what I want is to know if it's possible or not to accomplish the above.
Thanks in advance.
Sounds kind of like you're looking for Object.tap:
Yields x to the block, and then returns x. The primary purpose of this method is to “tap into” a method chain, in order to perform operations on intermediate results within the chain.
For your example, you might be able to use String's reverse! inside the tap to manipulate the object. For your application, manipulate the object as you desire inside tap, then the object will be passed on to your following method.