This document summarizes common Ruby "gotchas", or unexpected behaviors that can trip up developers. It covers topics like string interpolation requiring double quotes, truthiness of values other than false and nil, differences between symbols and strings, accessing characters in strings, variable scope in blocks, freezing arrays, and initializing arrays and hashes with the same object. The goal is to explain behaviors that may surprise those from other languages and help developers avoid potential pitfalls.
Double Revolving field theory-how the rotor develops torque
Ruby Gotchas
1. Ruby
Gotchas
Last edited 2014-10-05
by Dave Aronson,
T. Rex of Codosaurus, LLC
2. Ruby can be surprising!
Though "engineered to
maximize programmer
happiness", with the
"principle of least surprise",
Ruby still has gotchas.
This presentation will
proceed from newbie trivial
gotchas, to more advanced
and confusing gotchas.
We = 2
class Fixnum
def rb; self; end
end
We <3 .rb
=> true
But = 3
still = 1
perfect = 4
But - still .rb < perfect
=> true
3. Don't quote me on this, but . . . .
x = 3
puts 'x = #{x}nx'
x = #{x}nx
puts "x = #{x}nx"
x = 3
x
String interpolation
(including special chars
like n) fails with 'single'
quotes -- it requires
"double" quotes.
(Just like in most
languages with string
interpolation.)
To avoid: use doubles
whenever practical.
4. It's twue! It's twue!
Only two things are false
(falsey): false, and nil.
Everything else is true
(truthy), even 0 (false in C),
"" (false in JS), [], etc.
(Trips up people from C, JS,
etc. where some of these are
false.)
true ? "true" : "false"
=> "true"
false ? "true" : "false"
=> "false"
nil ? "true" : "false"
=> "false"
1 ? "true" : "false"
=> "true"
0 ? "true" : "false"
=> "true"
"false"? "true" : "false"
=> "true"
"" ? "true" : "false"
=> "true"
[] ? "true" : "false"
=> "true"
5. Hang him in effigy
(String him up, symbolically)
Symbols != strings.
Even if same, printed.
Remember which one to
use for what (args).
Ideally, take either: "Be
liberal in what you accept,
and conservative in what
you send." (Postel's Law)
str = "string"
sym = :string
puts str
string
puts sym
string
str == sym
=> false
7. Constants Aren't (Part 1/2)
FOO = 5
=> 5
FOO = 7
(irb):3: warning: already
initialized constant FOO
=> 7
FOO
=> 7
(Initial uppercase means
constant, in Ruby.)
Try to change a constant.
Ooooh, you get a
WARNING! BFD.
8. Constants Aren't (Part 2/2)
Even freezing doesn't
work for Fixnums.
It does work for arrays
(sort of) and most other
objects . . . he said
foreshadowingly.
FOO
=> 7
FOO.freeze
=> 7
FOO += 2
(irb):5: warning: already
initialized constant FOO
=> 9
FOO
=> 9
9. Some are more equal than others
Effectively:
== is the usual
(same value)
.eql? is value and class
(1 is Fixnum, 1.0 is Float)
.equal? is same object
It's actually much hairier;
see docs on class Object
1 == 1.0
=> true
1.eql? 1.0
=> false
a = "foo"
b = "foo"
a == b
=> true
a.eql? b
=> true
a.equal? b
=> false
a.equal? a
=> true
10. === != ==!
Effectively:
=== is "case equality", as in
case statements. A better name
(IMHO) might be ".describes?",
or overload ".includes?"!
Again, it's actually much hairier;
see the docs on class Object.
Gets people from languages
where === is identity, or same
value and class.
1 === 1
=> true
Fixnum === 1
=> true
1 === Fixnum
=> false
Class === Class
Object === Object
Class === Object
Object === Class
=> all true
Fixnum === Fixnum
=> false
(1..3) === 2
=> true
2 === (1..3)
=> false
11. and != &&
&& has higher precedence
than =, so
x = true && false
means
x = (true && false)
and has lower precedence, so
x = true and false
means
(x = true) and false
Ruby Style Guide: Use && / ||
for boolean expressions, [use]
and / or for control flow.
x = true && false
=> false
x
=> false
# OK so far, but:
x = true and false
=> false
x
=> true
Return value is false
but variable is true!
Why the mismatch?!
12. or != ||
|| has higher precedence
than =, so
x = false || true
means
x = (false || true)
or has lower precedence so
x = false or true
means
(x = false) or true
Also, && is higher than ||,
but and and or are equal, so
they are evaluated left-to-right!
x = false || true
=> true
x
=> true
# OK so far, but:
x = false or true
=> true
x
=> false
Return value is true
but variable is false!
Why the mismatch?!
13. Don't be so sensitive! (Part 1/4)
Whitespace-insensitive?
NOT ALWAYS!
With multiple args:
- No parens, no problem.
- Parens w/o space, OK.
- Parens and space, NO!
Parser thinks it's an
expression, as one arg,
but (1, 2) is not a valid
Ruby expression!
(All work fine w/ 1 arg.)
def method(arg1, arg2); end
method 1, 2
=> nil
method(1, 2)
=> nil
method (1, 2)
syntax error, unexpected
',', expecting ')'
method (1, 2)
^
14. Don't be so sensitive! (Part 2/4)
def method; 42; end
num = 21
method/num
=> 2
method / num
=> 2
method/ num
=> 2
method /num
SyntaxError:
unterminated regexp
"method /num" is an
unended regex or string!
Ruby thinks you might be
giving an argument to
method meth.
General principle: use
BALANCED whitespace;
both sides or neither.
15. Don't be so sensitive! (Part 3/4)
"one -1" makes Ruby
think you might be giving
an argument (of -1) to
method one. (Same for
+1 . . . or even *1!)
Again: use BALANCED
whitespace; both sides or
neither.
def one
1
end
one - 1
=> 0
one-1
=> 0
one- 1
=> 0
one -1
ArgumentError: wrong number
of arguments (1 for 0)
16. Don't be so sensitive! (Part 4/4)
dbl = ->(x) { x * 2 }
=> #<Proc:... (lambda)>
dbl = ->x{ x * 2 }
=> #<Proc:... (lambda)>
dbl = -> x { x * 2 }
=> #<Proc:... (lambda)>
two = -> { 2 }
=> #<Proc:... (lambda)>
dbl = -> (x) { x * 2 }
syntax error, unexpected
tLPAREN_ARG, expecting
keyword_do_LAMBDA or tLAMBEG
two = -> () { 2 }
same syntax error
"Stabby" lambdas (1.9+)
Parentheses optional
Space before/after args
without parens, OK.
Space after parens, OK.
Again, space before
parens, NO!
UPDATE: Fixed in 2.0!
17. 'Ang onto yer @!
class Foo
attr_reader :value
def initialize(v)
value = v
end
def set_val(v)
@value = v
end
end
f = Foo.new(3)
f.value
=> nil # not 3?!
f.set_val 5
=> 5
f.value
=> 5
Naked value becomes a
temporary local variable!
Solution: remember the @! (Or
"self.".)
Gets people from Java/C++,
not so much Python (which
needs "self." too).
"You keep on using that variable. I don't
think it means what you think it means."
18. Look out, it’s an @@!
What the fillintheblank? We didn't
change Parent’s @@value before
checking it, nor Child’s at all!
. . . Or did we?
@@ variables are shared with
subclasses -- not just that they
exist, but the variables
themselves! Declaring Child’s
@@value changed Parent’s, and
inc’ing Parent’s changed Child’s.
IMHO, best just forget them.
class Parent
@@value = 6
def self.value
@@value
end
def self.inc_value
@@value += 1
end
end
class Child < Parent
@@value = 42
end
Parent.value
=> 42 # wtf?
Parent.inc_value
Child.value
=> 43 # wtf?!
19. With init(ialize) or without it
class Parent
def initialize
puts "Parent init"
end
end
class NoInitChild < Parent
end
NoInitChild.new
Parent init
class NormalChild < Parent
def initialize
puts "NormalChild init"
end
end
NormalChild.new
"NormalChild init"
class SuperChild < Parent
def initialize
puts "SuperChild"
super
puts "init"
end
end
SuperChild.new
SuperChild
Parent init
init
Parent's initialize runs
automagically only if child
has none. Else, parent's
must be called to run.
20. Superman vs. the Invisible Man
Child2.new.add 1, 2, 3, 5
ArgumentError: wrong
number of arguments (4
for 2)
Child2.new.add 1, 2
=> 3
Child4.new.add 1, 2, 3, 5
=> 11
super with no arg list
sends what caller got
super with explicit args
sends those args
to send NO args, use
empty parens: super()
class Parent
def add *args
args.inject :+
end
end
class Child2 < Parent
def add arg1, arg2
super arg1, arg2
end
end
class Child4 < Parent
def add a1, a2, a3, a4
super # no args!
end
end
21. When will it end? (Or start?)
str = "OnenTwonThree"
str =~ /^Two$/
=> 4
str =~ /ATwoZ/
=> nil
str =~ /AOne/
=> 0
str =~ /ThreeZ/
=> 8
In "standard" regexps:
^ is start and $ is end...
of the whole string.
Ruby’s regexes default to
multiline, so:
^ is start and $ is end...
of any line!
A is start and Z is end
of the whole string. (Or z
to include any newline…
which is another gotcha!)
22. [].any?
=> false
[1].any?
=> true
[:foo, :bar].any?
=> true
# ok so far, BUT:
[nil].any?
=> false
[false].any?
=> false
[false, nil].any?
=> false
getting .any?
.any? does not mean
“any elements?”!
With block: “do any
make the block true?”
Without: “are any truthy?”
Has implicit block:
{ |element| element }
23. Variables declared in blocks
passed to iterators (e.g.,
times or each) are undefined
at the top of each iteration!
Iterators call the block
repeatedly, so vars are out of
scope again after each call.
Built-in looping constructs (e.
g., while or for) are OK.
(Or declare vars before block.)
3.times do |loop_num|
sum ||= 0
sum += 1
puts sum
end
111
for loop_num in 1..3
sum ||= 0
sum += 1
puts sum
end
123
(Un)Def Leppard
24. Freeze (Ar)ray
arr = ["one", "two", "three"]
arr.freeze
arr << "four"
RuntimeError: can't modify
frozen Array
arr[0] = "eno"
RuntimeError: can't modify
frozen Array
arr[0].object_id
=> 1234567890
arr[0].reverse!
arr
=> ["eno", "two", "three"]
arr[0].object_id
=> 1234567890
Freezing an array (or a
hash) freezes it, not the
items it contains.
Strings can be modified
in place. This way, you
can modify a given slot in
a frozen Array of Strings.
25. 1 is 1 … and ever more shall be so!
Changing Fixnum to new
value means new object.
They can't be modified in
place! So, can’t modify a
frozen Array of Fixnums.
(Fixnums and Integers
have no bang-methods to
demo trying with.)
BTW: a Fixnum's object_id
is value * 2 + 1.
arr = [1, 2, 3, 4]
arr.freeze
=> [1, 2, 3, 4]
arr << 5
RuntimeError: can't modify
frozen Array
arr[0] += 2
RuntimeError: can't modify
frozen Array
1.object_id
=> 3
3.object_id
=> 7
26. (to! || ! to!) == ?
str = "foo"
str.upcase
=> ”FOO”
str
=> ”foo”
str.upcase!
=> ”FOO”
str
=> ”FOO”
# Now that it’s already FOO:
str.upcase!
=> nil # ?!
str
=> ”FOO”
Well-known semi-gotcha:
bang versions of methods
are dangerous; usually
may modify receiver.
DO NOT RELY ON THEM
RETURNING SAME
VALUE AS NON-BANG
VERSION!
Many return nil if no
change needed!
27. An Array of New Gotchas
Initial value given as
object is same object
for each slot (if modded
in place, not reassigned
as with = or +=).
Initial value given as
block gets evaluated
separately for each slot.
Use this to create new
vars for each.
class Person
attr_accessor :name
end
people = Array.new(3, Person.new)
people[0].name = "Alice"
people[1].name = "Bob"
people[0].name
=> "Bob"
# should have been "Alice"!
people = Array.new(3) { Person.new
}
people[0].name = "Alice"
people[1].name = "Bob"
people[0].name
=> "Alice"
28. Making a Hash of it
Mostly same problem (and
solution) as Arrays.
WARNING: creates new
object on any access to
empty slot! May create
excessive number of new
objects; ruins checking
“real” contents or count
(nil-checking, .size, etc.).
langs = Hash.new []
langs[:jane] << "Java"
langs[:rachel] << "Ruby"
langs[:jane]
=> ["Java", "Ruby"]
langs[:rachel]
=> ["Java", "Ruby"]
langs = Hash.new { |h, k|
h[k] = [] }
langs[:jane] << "Java"
langs[:rachel] << "Ruby"
langs[:jane]
=> ["Java"]
langs[:rachel]
=> ["Ruby"]
29. Rescue Me, Throw a Line, I'll Try to Catch It!
/* JAVA: */
try {
throw new MyException("blah");
} catch(MyException e) {
fix_it();
}
# RUBY:
index = catch(:idx) {
arr.each_with_index do |v, i|
throw :idx, i if v == target
end
-1
}
begin
raise MyException.new "blah"
rescue MyException => e
fix_it
end
In Ruby, throw and catch
are NOT for exceptions!
They are advanced flow
control, to exit deep
nesting.
Ruby uses raise and
rescue for exceptions.
30. I’m Gonna Getcha Getcha Getcha Getcha!
- Watch out for these gotchas as you code.
- If Ruby behaves badly, refer to these slides.
- Available at http://bit.ly/RubyGotchas
- If your gotcha isn’t listed, tell me; maybe I’ll add it!
31. The Someday-Maybe List
Add gotchas:
- to_s vs. to_str
- need to coordinate method_missing and responds_to_missing
- rescue from a StandardError, not an Exception
- instance_eval with calls in local scope
- private data isn’t really, and not at all w/ class methods
- lambda vs. proc vs. block vs. method vs. Bambi vs. Godzilla
- attribute=(val) always returns the argument, no matter the code
- Proxies are always truthy, even if the target is not
- class Foo::Bar, defined outside Module Foo, won’t see inside Foo
- in debugging, “next” goes into loops but skips over blocks
- vars introduced in a loop (not block!) are visible outside it (?)
- private methods are accessible by the instance, not the whole class
Rails gotchas?
Screencast series?
32. Questions/Contact/Etc.
questions.any? ; gotchas[:more].any?
Contact information / shameless plug:
T.Rex-2015 [at] Codosaur [dot] us
+1-571-308-6622
www.Codosaur.us (Codosaurus, LLC main site)
www.linkedin.com/profile/view?id=719998
Blog.Codosaur.us (code blog)
www.Dare2XL.com (excellence blog)
@davearonson
AVAILABLE FOR CONSULTING!