Support Ukraine. DONATE.
A blog about software development.

Unexpected Ruby behaviour

Serhii Potapov August 10, 2012 #unexpected #ruby #behaviour #lambda #proc #time #ensure #DelegateClass #regexp #super

Ruby is a cool language with intuitive grammar. However there are a number of things which don't seem to be expected. It might take long hours to debug some weird issues for unenlightened newbies.

Implicitly variable declaration

Variable mentioned in conditional block of code become declared and initialized with nil even if the declaration was not executed.

if false
  var = "never executed"
end
var # => nil

Expected: addressing to var raises NameError: undefined local variable or method 'var'

Calling #utc and #gmt on Time object removes time zone information

t = Time.new  # => Sat Aug 11 01:11:52 +0300 2012
t.utc         # => Fri Aug 10 22:11:52 UTC 2012
t             # => Fri Aug 10 22:11:52 UTC 2012, WTF? O_o

t = Time.new  # => Sat Aug 11 01:17:06 +0300 2012
t.gmtime      # => Fri Aug 10 22:17:06 UTC 2012
t             # => Fri Aug 10 22:17:06 UTC 2012, WTF? O_o

IMHO, this methods should be called utc! and gmtime! instead. I have an experience when it caused a really voodoo thing: a test failed only from 20:00 to 00:00 in USA on CI server, and could never be reproduced in my time zone.

Instead it's better to use getutc and getgm methods which return UTC and GMT time accordingly, but don't change Time object:

t = Time.new  # => Sat Aug 11 01:11:52 +0300 2012
t.getutc      # => Fri Aug 10 22:11:52 UTC 2012
t             # => Sat Aug 11 01:11:52 +0300 2012

t = Time.new  # => Sat Aug 11 01:17:06 +0300 2012
t.getgm       # => Fri Aug 10 22:17:06 UTC 2012
t             # => Sat Aug 11 01:17:06 +0300 2012

Methods do not return value from ensure statement

Usually ruby methods return the value of the last method line unless return is called explicitly. But how about this?

def run
  1
ensure
  puts "ensure block..."
  2
end

run # => 1
# pints `ensure block...`

So if you want to return a value from ensure statement use return word:

def run
  1
ensure
  return 2
end

run # => 2

Anchors ^ and $ do not mean a start and an end of a string

Most of script languages uses anchors ^ and $ of regular expressions as a start and an end of string accordingly. But not in Ruby! In Ruby they are a start and an end of a line. See the difference:

pattern      = /^[a-zA-Z]{3,12}$/    # 3-12 alphabetic characters
valid_name   = "Tatiana"
invalid_name = "23\nabc\n!"

valid_name   =~ pattern   # => 0, matchers
invalid_name =~ pattern   # => 3, matchers

Instead use \A and \z anchors. They are a start and an end of a string.

pattern      = /\A[a-zA-Z]{3,12}\z/    # 3-12 alphabetic characters
valid_name   = "Tatiana"
invalid_name = "23\nabc\n!"

valid_name   =~ pattern   # => 0, matchers
invalid_name =~ pattern   # => nil

Pay attention when you write validations. Read Egor Homakov's article to get more information about it.

\m regexp option

When other languages use \s option to make . match newline, Ruby uses \m:

 "\n" =~ /./   # => nil
 "\n" =~ /./m  # => 0

Calling super and super() are not the same

There is no matter for ruby methods do you use parentheses or not. But be careful with super since it's not a method, but a key word. Let me show an example when parentheses matter:

class Parent
  def m1(arg)
    puts "Parent m1: arg = #{arg.inspect}"
  end

  def m2(arg)
    puts "Parent m2: arg = #{arg.inspect}"
  end
end

class Child < Parent
  def m1(arg)
    puts "Child m1: arg = #{arg.inspect}"
    super
  end

  def m2(arg)
    puts "Child m2: arg = #{arg.inspect}"
    super()
  end
end

child = Child.new
child.m1("foo")
child.m2("bar")

Output:

Child m1: arg = "foo"
Parent m1: arg = "foo"
Child m2: arg = "bar"
super.rb:6:in `m2': wrong number of arguments (0 for 1) (ArgumentError)
        from super.rb:19:in `m2'
        from super.rb:25:in `<main>'

When you use super it calls same method of parent class passing same arguments to it. But when you use super(...) you have to pass arguments manually. In my example ArgumentError was raised because Parent#m2 expects to receive exactly one argument, but nothing was passed to super()

However super() still delegates a passed block. If you don't wanna pass a block you have to do it explicitly using super(&nil):

class Parent
  def m1
    yield
  end

  def m2
    yield
  end
end

class Child < Parent
  def m1
    super()
  end

  def m2
    super(&nil)
  end
end

child = Child.new
child.m1 { puts "Hi, m1" }
child.m2 { puts "Hi, m2" }

Output: Hi, m1 /tmp/super.rb:7:in `m2': no block given (yield) (LocalJumpError)

lambda and Proc.new act differently

It's a well known thing but I want to remind. There 2 differences between proc objects created with lambda and Proc.new:

lm = lambda   {|a, b| "#{a.inspect} and #{b.inspect}" }
pr = Proc.new {|a, b| "#{a.inspect} and #{b.inspect}" }

lm.call(10)  # => ArgumentError: wrong number of arguments (1 for 2)
pr.call(10)  # => "10 and nil"
def lambda_method
  lm = lambda { return 10 }     # return from lambda
  half = lm.call
  half * 2
end

def proc_method
  pr = Proc.new { return 10 }   # return from proc_method
  half = pr.call
  half * 2
end

lambda_method   # => 20
proc_method     # => 10

Note there is also method proc. In Ruby 1.8 it's a synonym for lambda but in Ruby 1.9 it's a synonym for Proc.new. So avoid using proc to keep you code compatible for both ruby versions.

DelegateClass instance does not eql itself

Ruby standard library provides DelegateClass which can be pretty useful. But some things are not so obvious about it:

require 'delegate'

class Animal
end

class Dog < DelegateClass(Animal)
end


animal = Animal.new
dog = Dog.new(animal)

dog.eql?(dog)  # => false, WTF? O_o

It happens because eql? is delegated to base object(animal):

dog.eql?(animal)  # => true

On other hand equal? is not:

dog.equal?(dog)     # => true
dog.equal?(animal)  # => false

dup method do not work for all objects

As you might know every object in Ruby has dup method inherited from Object class. It's convenient, I like it. But it does not mean that every object can be duplicated.

[:symbol, true, false].all? { |obj| obj.respond_to? :dup } # => true
:symbol.dup   # => TypeError: can't dup Symbol
true.dup      # => TypeError: can't dup TrueClass
false.dup     # => TypeError: can't dup FalseClass

It means you can't make a deep copy of an array (with one level depth) like this:

copy = array.map(&:dup)

Or like this:

copy = array.map { |val| val.respond_to?(:dup) ? val.dup : val }

Since all your objects respond to #dup.

ActiveSupport provides duplicable? method to inspect an ability to duplicate an object, so finally your solution would look the next way:

copy = array.map { |val| val.duplicable? ? val.dup : val }

Internally ruby symbols and booleans point to constant memory addresses, and there is no reason to copy them since they are unchangable. But I wish the implementation of their #dup methods looked like this:

class Symbol
  def dup
    self
  end
end


So I hope the article was useful for you. If you know some fancy Ruby things I haven't mentioned please let me know.

Thanks.

Back to top