Two similar sentences have different behaviour. Is it ok?
Compare:
a = 123 unless defined? a
a # => nil
but...
unless defined? b
b = 123
end
b # => 123
Yes, this is the correct behaviour. Local variables are created and initialized with nil before assignment. So this code
a = 123 unless defined? a
a # => nil
is a rough equivalent of
a = nil
a = 123 unless defined? a # `a` is not undefined anymore.
a # => nil
Another example (even though c is not defined before this line, this code does not throw a NameError).
c = 2 unless c # => 2
Related
This question already has answers here:
Confusion with the assignment operation inside a falsy `if` block [duplicate]
(3 answers)
Why is `a = a` `nil` in Ruby?
(1 answer)
Closed 8 years ago.
This is something strange I've figured out in ruby 1.9.3.
Here is the code:
>> r = true
>> if r
>> a = "hello"
>> else
>> b = "hello"
>> end
Now the value of a is "hello":
>> a
=> "hello"
And strangely the value of b is nil
>> b
=> nil
Since b is nowhere in the scene, it should be undeclared.
Why?
Variable declarations take effect even if the branch where the assignment takes place is never reached.
The reason for this is explained in the Ruby documentation, and it is valid for any version of Ruby, not just 1.9:
The local variable is created when the parser encounters the
assignment, not when the assignment occurs.
This means that if the parser sees an assignment in the code it creates the local variable even if the assignment will never really occur. In this case the value referenced by the variable is nil:
if true
a = 0
else
b = 0
end
p local_variables
# => [:a, :b]
p a
# => 0
p b
# => nil
The local variable is created when the parser encounters the
assignment, not when the assignment occurs:
=> foo
# NameError: undefined local variable or method `foo' for main:Object
=> if false
=> foo = "bar" # does not assign to foo
=> end
=> nil
=> foo
=> nil
I have #obj.items_per_page, which is 20 at the beginning, and I want the method below to assign value to it only if many_items is not nil:
def fetch_it_baby (many_items = nil)
#obj.items_per_page = many_items
With the code above, even if many_items is nil, #obj.items_per_page remains at 20. Why? And is that "good" coding? Shouldn't I use something like
#obj.items_per_page = many_items || #obj.items_per_page
Or is there a third way? I don't feel completely comfortable with either way.
The style I generally see looks like this:
#obj.items_per_page = many_items if many_items
This uses the inline conditional, while avoiding negative or double-negative conditions.
I suggest the following as it makes it clear that you have a default value for the assignment in case the caller did not specify many_items in the call:
def function(argument = nil)
variable = argument || 20
...
end
However, since you specified that the assignment takes places only if the value is not nil then you'll need to check for the nil value otherwise you will miss the assignment if the value was false. If you really need that case then the solution is more long-winded:
def function(argument = nil)
variable = argument.nil? ? 20 : argument
...
end
You can use &&= (in the same way as ||= is used to assign only if nil or false)
> a = 20 # => 20
> a &&= 30 # => 30
> a # => 30
> a = nil # => nil
> a &&= 30 # => nil
> a = false # => false
> a &&= 30 # => false
> a = {} # => {}
> a &&= 30 # => 30
remember though
> a = 30 # => 30
> a &&= nil # => nil
> a &&= false # => nil
> b &&= 3 # => nil
Even if many_items is nil #obj.items_per_page remains at 20
That sounds like whatever class #obj is has a custom modifier method items_per_page= that only updates the value if the new value is not nil. This is not standard Ruby. For example, given this definition:
class Demo
attr_accessor :items_per_page
end
I get this behavior:
irb(main):005:0> demo = Demo.new #=> #<Demo:0x007fb7b2060240>
irb(main):006:0> demo.items_per_page = 20 #=> 20
irb(main):007:0> demo.items_per_page #=> 20
irb(main):008:0> demo.items_per_page = nil #=> nil
irb(main):009:0> demo.items_per_page #=> nil
As for your example, I would probably write it this way:
#obj.items_per_page = many_items unless many_items.nil?
For Rails you can also use presence as described here
region = params[:state].presence || params[:country].presence || 'US'
new-alexandria's answer is my go-to, but another "third way" would be to use a ternary:
class Demo
attr_accessor :items_per_page
end
many_items = 100
#obj = Demo.new
#obj.items_per_page = 20 #=> 20
#obj.items_per_page = !many_items.nil? ? 30 : nil #=> 30
I am using Rails and I have a similar need.
You can define a method on your model:
class Gift < ApplicationRecord
def safe_set(attribute, value)
return if value.nil?
send("#{attribute}=", value)
end
end
So you can do
g = Gift.new
g.colour = 'red'
g.safe_set(:colour, nil)
g.colour -> 'red'
g.safe_set(:colour, 'green')
g.colour -> 'green'
We have one more method in rails that can help to remove values that are nil.
compact
This method can be used with an array, hash. Returns the same data removing all values that are nil
Eg :
array.compact
Further reference:
https://apidock.com/ruby/v1_9_3_392/Array/compact
If many items is a variable the if/unless versions above are the best. But if many_items is actually a method you don't want to evaluate multiple times I find the following useful.
#obj.items_per_page = many_items.presence || #obj.items_per_page
This isn't quite what you want since it won't assign empty strings or other non-nil but non-present values but most of the time I want to do this kind of thing this works for me (it does create a self-assignment when the value is nil/non-present so only use it if your setters are idempotent).
Is there any difference between using the || operator and rescue in Ruby?
Say:
b = A.value || "5"
b = A.value rescue 5
where the object A does not have value method.
|| is the boolean or operator (keep in mind that in Ruby, only the values nil and false evaluates to false, in boolean context):
nil || 5
# => 5
false || 5
# => 5
4 || 5
# => 4
rescue is for exception catching:
fail 'bang' rescue 5
# => 5
'bang' rescue 5
# => "bang"
nil rescue 5
# => nil
In your examples, given that A do not respond to value:
A.value
# NoMethodError: undefined method `value' ...
b = A.value || 5
# NoMethodError: ...
b
# => nil
b = A.value rescue 5
b
# => 5
Now suppose that A.value returns nil:
A.value
# => nil
b = A.value || 5
b
# => 5
b = A.value rescue 5
b
# => nil
The || is an or operator. Your first line reads:
Set b to A.value; if not b (i.e. b is nil or false), then set it to the string "5".
Rescue lets you recover from exceptions. Your second line reads:
Set b to A.value. If A.value raises an exception, ignore the problem and set b to 5 instead.
For an object A with no value method, the first line will crash the app.
For an object A whose value method returns nil, the second line will set b to nil.
Apart from what others already told you, one more difference is, that || is an honest operator, while inline rescue is tantamount to rescuing StandardError, which is someting that various manuals of style passionately frown upon. In other words, inline rescue is an indecent hack, that should not be used too frequently in production code. Use decently dressed begin ... rescue ... else ... ensure ... end statement instead.
I was under the impression that conditions joined with && were executed in sequence, such that the following would return true:
a = "adasd"
> b = a && b.present?
=> false
Thoughts?
Thanks!
--Peter
note:
b = a
=> "adasd"
b.present?
=> true
When you say this:
b = a && b.present?
You're declaring b as a local variable but it will be nil until the right side of the assignment is evaluated. In particular, b will be nil when you call present? on it and the conjunction will be false making b false.
When you do this:
a = 'pancakes'
b = a
b.present?
b will have the value 'pancakes' when you call present? on it so you get a true return from b.present?.
As per the rails doc
present is checking for a variable to be non blank
As per assignment timing, ruby declares the variable in the scope as soon as it sees it, so b will be in the scope but with no value, so present will return false.
You should maybe compared this with defined?
a = "abc"
=> "abc"
defined? a
=> "local-variable"
defined? b
=> nil
b = defined? b
=> "local-variable"
In Ruby, how do you set a variable to a certain value if it is not already defined, and leave the current value if it is already defined?
While x ||= value is a way to say "if x contains a falsey value, including nil (which is implicit in this construct if x is not defined because it appears on the left hand side of the assignment), assign value to x", it does just that.
It is roughly equivalent to the following. (However, x ||= value will not throw a NameError like this code may and it will always assign a value to x as this code does not -- the point is to see x ||= value works the same for any falsey value in x, including the "default" nil value):
if !x
x = value
end
To see if the variable has truly not been assigned a value, use the defined? method:
>> defined? z
=> nil
>> z = nil
=> nil
>> defined? z
=> "local-variable"
>> defined? #z
=> nil
>> #z = nil
=> nil
>> defined? #z
=> "instance-variable"
However, in almost every case, using defined? is code smell. Be careful with power. Do the sensible thing: give variables values before trying to use them :)
Happy coding.
#variable ||= "set value if not set"
So false variables will get overridden
> #test = true
=> true
> #test ||= "test"
=> true
> #test
=> nil
> #test ||= "test"
=> "test"
> #test = false
=> false
> #test ||= "test"
=> "test"
As you didn't specify what kind of variable:
v = v
v ||= 1
Don't recommend doing this with local variables though.
Edit: In fact v=v is not needed
If the variable is not defined (declared?) it doesn't exist, and if it is declared then you know how you initialized it, right?
Usually, if I just need a variable whose use I don't yet know---that I know will never use as a Boolean---I initialize it by setting its value to nil. Then you can test if it has been changed later quite easily
x = nil
some code
if x do
[code that will only run if x has changed]
end
that's all.