Variable assignment inside of ruby methods - ruby

I have this pretty simple method:
def update_context(msg, session, sender)
previous_context = session.context
session.update(context: intent_determination(msg, session.context, sender))
session.update(context: brand_determination(msg, session.context))
session.update(context: style_determination(msg, session.context))
session.update(context: price_range_determination(msg, session.context))
session.update(context: size_determination(msg, session.context))
p previous_context
p session.context
p (previous_context == session.context)
unless session.context.size == 0
if previous_context == session.context
session.context["intent"] = "lost"
session.save
end
end
end
My problem is certainly due to a stupid mistake I can't see but please bear with me on this one, I really can't see it.
As you can see, I "save" the session's context in a previous_context variable at the beginning of the method. Then, I'm running a few updates on the context.
However, when I print previous_context, session.context and previous_context == session.context, I get the same result for the first two, and true for the last one.
How is this possible ? I assigned to previous_context the value of session.context before updating it. And then, previous_context has the same value as session.context after I've updated it.
I really can't see where I screwed up here, or there is definitely something I don't understand.

previous_context = session.context makes the previous_context variable point to the same object as session.context. If you want to change one without affecting the other, you'll need to create a copy of session.context to store in previous_context.

in Ruby, variables are just references to objects, so what you are doing there is merely creating a new reference to the same object. If you wish to save the previous state you will have to copy the entire object.
See this answer for a more graphic explanation.

Related

Using slice! on a variable is modifying the node attribute that populated the variable

In OpsWorks Stacks, I have set a layer attribute using the custom JSON field:
{
"layer_apps" : [
"app_manager"
]
}
The app_ portion of the attribute is necessary for the workflow. At times, I need to temporarily remove the app_ portion within a cookbook. To do this, I use slice!:
node['layer_apps'].each do |app_name|
install_certs_app_name = app_name
install_certs_app_name.slice!('app_') # 'app_manager' => 'manager'
# snip
end
However, once this is done, even though app_name isn't being directly modified, each node['layer_apps'] attribute gets sliced, which carries on to subsequent cookbooks and causes failures. The behaviour I expected was that slice! would modify app_name, and not the current node['layer_apps'] attribute. Thinking that app_name was a link to the attribute rather than being it's own variable, I tried assigning its value to a separate variable (install_certs_app_name and similar in other cookbooks), but the behaviour persisted.
Is this expected behaviour in Ruby/Chef? Is there a better way to be excluding the app_ prefix from the attribute?
app_name is being directly modified. That's the reason for the bang ! after the method... so that you're aware that the method mutates the object.
and app_name and install_certs_app_name are referencing the same object.
Note that slice and slice! both return "app_" but the bang object mutates the caller by removing the sliced text.
If you did
result = install_certs_app_name.slice!('app_')
puts result
==> app_
puts install_certs_app_name
--> manager
Try (instead)
install_certs_app_name = app_name.dup
install_certs_app_name.slice!('app_')
So you have two separate objects.
Alternatively,
install_certs_app_name = app_name.sub('app_', '')
In case you'd want a variable sliced, what you'll is the non-destructive version:
str.slice and not str.slice!
These are often referred to as Bang-methods, and replace the variable in place.
Below is an example with the .downcase method. This is the same principle for .slice.
EDIT:
However, since .slice returns the part that's been cut out, you could just remove the app_-part .sub like
"app_manager".sub("app_",'') #=> "manager"
http://ruby-for-beginners.rubymonstas.org/objects/bangs.html
https://ruby-doc.org/core-2.2.0/String.html#method-i-slice
When you assigning app_name to install_certs_app_name you still referencing to the same object. In order to create new object you can do:
install_certs_app_name = app_name.dup
New object with the same value is created. And slicing install_certs_app_name does not affect app_name this way.

Create an object if one is not found

How do I create an object if one is not found? This is the query I was running:
#event_object = #event_entry.event_objects.find_all_by_plantype('dog')
and I was trying this:
#event_object = EventObject.new unless #event_entry.event_objects.find_all_by_plantype('dog')
but that does not seem to work. I know I'm missing something very simple like normal :( Thanks for any help!!! :)
find_all style methods return an array of matching records. That is an empty array if no matching records are found. And an empty is truthy. Which means:
arr = []
if arr
puts 'arr is considered turthy!' # this line will execute
end
Also, the dynamic finder methods (like find_by_whatever) are officially depreacted So you shouldn't be using them.
You probably want something more like:
#event_object = #event_entry.event_objects.where(plantype: 'dog').first || EventObject.new
But you can also configure the event object better, since you obviously want it to belong to #event_entry.
#event_object = #event_entry.event_objects.where(plantype: 'dog').first
#event_object ||= #event_entry.event_objects.build(plantype: dog)
In this last example, we try to find an existing object by getting an array of matching records and asking for the first item. If there are no items, #event_object will be nil.
Then we use the ||= operator that says "assign the value on the right if this is currently set to a falsy value". And nil is falsy. So if it's nil we can build the object form the association it should belong to. And we can preset it's attributes while we are at it.
Why not use built in query methods like find_or_create_by or find_or_initialize_by
#event_object = #event_entry.event_objects.find_or_create_by(plantype:'dog')
This will find an #event_entry.event_object with plantype = 'dog' if one does not exist it will then create one instead.
find_or_initialize_by is probably more what you want as it will leave #event_object in an unsaved state with just the association and plantype set
#event_object = #event_entry.event_objects.find_or_initialize_by(plantype:'dog')
This assumes you are looking for a single event_object as it will return the first one it finds with plantype = 'dog'. If more than 1 event_object can have the plantype ='dog' within the #event_entry scope then this might not be the best solution but it seems to fit with your description.

DataMapper first_or_create doesn't work... any ideas why?

Well, I've finally decided that I'm not crazy. So, that leaves DataMapper.
Here's what I'm doing. I have a model Msrun which has 1 Metric.
tmp = Msrun.first_or_create # I'll skip the boring details
tmp.metric = Metric.first_or_create( {msrun_id: tmp.id}, {metric_input_file: #metricsfile} )
p tmp.metric # => #<Metric #metric_input_file=nil #msrun_id=1>
tmp.metric.metric_input_file = #metricsfile
p tmp.metric # => #<Metric #metric_input_file=#<Pathname:/home/ryanmt/Dropbox/coding/rails/metrics_site/spec/tfiles/single_metric.txt> #msrun_id=1>
So, why doesn't this work? I'm reading http://datamapper.org/docs/create_and_destroy and doing what it shows working. This has been terribly arduous. Thanks for any help.
Update:
I still can't figure out what is going on, but to prove I'm not insane...
puts Metric.all # => []
tmp.metric = Metric.first_or_create( {msrun_id: tmp.id}, {metric_input_file: #metricsfile} )
puts Metric.all # => [] #??????????????
tmp.metric.metric_input_file = #metricsfile
p tmp.metric # => #<Metric #metric_input_file=#<Pathname:/home/ryanmt/Dropbox/coding/rails/metrics_site/spec/tfiles/single_metric.txt> #msrun_id=1>
tmp.metric.save
puts Metric.all # => [#<Metric #metric_input_file=#<Pathname:/home/ryanmt/Dropbox/coding/rails/metrics_site/spec/tfiles/single_metric.txt> #msrun_id=1>]
So, not only is first_or_create not delivering on the behavior I expect by reading the source
def first_or_create(conditions = {}, attributes = {})
first(conditions) || create(conditions.merge(attributes))
end
but it is also not even creating.
I'm probably missing something here (more of those boring details might help) but if the metric exists, it's metric_input_file shouldn't be updated, i.e., it's only set when new. If you're after updating then you can do
.first_or_create(msrun_id: tmp.id).update(metric_input_file: #metricsfile)
Or if not hitting the database twice is relevant, then
m = Metric.first_or_new(msrun_id: tmp.id)
[set..save..assign]
But if it's not being set on new models, I don't see what would cause that from the code posted so far, more..?
[UPDATED]
Based on your new code, I'd say this is "a classic case" of a false DM save. I usually add the following line to an initialization section, e.g., application.rb in Rails.
DataMapper::Model.raise_on_save_failure = true
Unfortunately, the exception raised never tells you why (there's a special place in hell for that choice, right next to people who talk in theaters.) But it's typically one of:
a slightly incorrect association definition
a has/belongs_to that isn't "required: false" and isn't set
putting the wrong datatype into a field, e.g., a string into a decimal
a validation failing
If you want to post your model definitions, the problem may be spottable there.
In addition to the answer above, I've seen this call die (like, literally halt all execution) with no error when I was doing a find_or_create that would have created an object that violated the primary key constraint. This is because the datamapper model was not in sync with the actual database schema.

Single Ruby Value in One Line From a Collection

I have a collection of objects. There are 3 properties in each object
'id', 'name', 'is_primary'
The collection of objects will usually have anywhere from 1 to 5 objects.
What I want to do is check the collection to see if is_primary is true. If so output the name, or at least return it.
I want to do this in 1 line of code if possible. I am trying to slim up this one line for erb output in rails. Later in the page i'll output them all. I thought I had it, but if I return nil it adds extra space which shifts all the html oddly.
Thanks.
Hmm, this doesn't quite work if no element is_primary...I'm still thinking...
c.detect(&:is_primary).name
Ok, how about:
((a = c.detect(&:is_primary)) && a.name).to_s
As it happens, it is OK in an erb template for the <%= expression to return nil, that just results in an empty string, so for that case you can use:
(a = c.detect(&:is_primary)) && a.name
Update: Responding to the first comment, I do have a test case that I didn't post...
class A; attr_accessor :is_primary, :name, :id; end
t = [A.new, A.new, A.new, (a = A.new; a.name = 'xyz'; a.is_primary = true; a)]
puts (a = t.detect(&:is_primary)) && a.name
puts ((a = [].detect(&:is_primary)) && a.name).to_s
Complementing #DigitalRoss, you can also write:
collection.detect(&:is_primary).try(:name) || "default_if_no_element_or_name"
(well, to be honest I prefer Ick's maybe over Rails' try: c.detect(&:is_primary).maybe.name)
Side note: IMHO a flag that can only be active for a row it's not such a good idea. You may have inconsistent states with more than one being active and you'll have worry about it when updating (transactions, and so on). Try to store the PK reference somewhere else (a parent model? a state model?).
I want to do this in 1 line of code if possible. I am trying to slim up this one line for erb output in rails. Later in the page i'll output them all.
No need for one-liners (funny since I just wrote one): move the code to yous models or helpers as appropriate and keep your views pristine.

Is 'buggy_logger' a reference to the 'status' string in this Ruby example?

In this example, do the nukes get launched because any changes that you make to buggy_logger get applied to the 'status' string - just like using a copy of a reference to an object -> when you make a change to the copy of the reference, the change gets applied to the underlying object -> that change is, in turn, reflected in any other references to the object. So, in other words, buggy_logger is an alias to the 'status' object without specifically using the alias keyword? Is that correct? So, in ruby, you just say
b = a
and then any changes you make to b afterwards are also reflected in a. Or is this only true because we're talking about Strings, which are mutable in Ruby?
# example-4.rb
status = "peace"
buggy_logger = status
print "Status: "
print buggy_logger << "\n" # <- This insertion is the bug.
def launch_nukes?(status)
unless status == 'peace'
return true
else
return false
end
end
print "Nukes Launched: #{launch_nukes?(status)}\n"
# => Status: peace
# => Nukes Launched: true
Yes, it is because strings are objects. Try
buggy_logger = status.dup
If you want a distinct object with the same initial value.
As for your question about alias I suspect you aren't correctly understanding how alias is used in ruby; it's used on methods, not objects, and isn't related to mutability.
Note also that the same semantics would have applied with any class; if status had been an array, a file, or anything else (provided it had mutable state suitable for use as a logger), you would have gotten analogous results.
One warning about dup though. If your object refers to other objects, the copy will also refer to the same objects. It's fine once you start thinking about it the right way, but tricky till then.

Resources