Quần Cam

Six confusing features in Ruby

I love the Ruby language, undoubtedly! When you love something, you should love not only the bright side but also the dark side of it too, unconditionally. Probably that is the message Katy Perry tries to deliver.

In this post I am trying to point out some Ruby features you might want to use with a lot of caution.

I have another post about five gotchas in Rails because I love the framework too.

1. The [] method

I really have no clue why Rubyists love the use of [] so much.

As in other programming language, it can be used to access Array elements.

array = [1, 2, 3]
array[0] # => 1

and Hash entries.

hash = {foo: "bar"}
hash[:foo] # => "bar"

You can also get the character at a given index of a String with [].

"Hello World"[0] # => "H"

And most confusingly, you invoke a Proc or Lambda by [].

hello = (-> (name) { "Hi, #{name}!" })
hello["John"] # => "Hi, John"

What if we chain them together?

wise_words_factory = (-> (number_of_elements) { (1..number_of_elements).map { WideWord.random } })

wise_words_factory[10][0][:category] # "Body builder"
wise_words_factory[10][0][:words] # "No pain, no gain"
wise_words_factory[10][0][:words][0] # "N"

Have a good time debugging, what could possibly go wrong?

2. The % operator

Like [], % in Ruby is bewildering as hell.

% can be used as modulo operator.

103 % 100 # => 3

We can also use % to format string, where the confusion arises.

"%s: %d" % ["age", 18] # => age: 18

To avoid confusion, using the alternative Kernel.format will result the same.

Kernel.format(format, "age", 18) # => age: 18

3. The Integer#zero? method

If you are unsure of what zero? is about, let us do a simple walkthrough.

assert_equal(1 == 0, 1.zero?) # => true

This looks cool in the first sight, using zero? seems to make code look more concise, beautiful and readable.

But in the end of the day, this causes more confusion than help, because they are … NOT EQUAL. Using == 0 is about doing equality comparison on the objects with all physical and semantic check, while .zero?, in OOP terminology, sends a message to the callee and requires the callee to be a number, even though it might proxy back to == eventually.

Consider the example below, the first example will fail and raise exception if number is not a number.

def x_or_o(number)
  number.zero? ? "o" : "x"
end

def x_or_o(number)
  number == 0 ? "o" : "x"
end

Just use == 0, it is TOTALLY readable, and NOT ugly at all.

4. The $[number] global variables

Consider this regex matching.

# test.rb
string = "Hi, John!"
matched = %r(^(.+), (.+)!$).match(string)
matched[0] => "Hi, John!"
matched[1] => "Hi"
matched[2] => "John"

Looks pretty neat, right? But wait, Ruby provides another way to retrieve the matched data too.

string = "Hi, John!"
%r(^(.+), (.+)!$).match(string)
$1 => "Hi"
$2 => "John"

“Mutating global variables for every regex matching is horrible and error prone and race and such!”, you might scream. Nevertheless Ruby has you covered, in a way. According to the documentation, these global variables are thread-local and method-local variables. So generally, they are not global.

AHA moment #1: Why call them global while they are not?

When I thought I could apply the same thing to matched[0], magic happened.

$0 # => test.rb

AHA moment #2: $0 in Ruby is reserved as the global variable for the current filename.

And I suddenly remembered in Ruby I could use use negative number to go backward the array. Let’s give it a try.

matched[-1] # => "John"
$-1 # => nil

OMG it worked!

AHA moment #2: #TIL $-1, even though it will not give you any matched data.

You can even assign value to $-1.

$-1 = 100
$-1 # => 100

Awesome, let’s go further.

$-100 = 100

#SyntaxError: (irb):29: syntax error, unexpected tINTEGER, expecting end-of-input
#$-100
#     ^
#   from /usr/local/bin/irb:11:in `<main>'

AHA moment #3: #TIL $-[number] only works when number is in 1-9.

Alright let’s end here. ¯\_(ツ)_/¯.

5. The masterpiece of the omnipotent God: Time.parse

Time.parse is a very powerful time parser, with many time formats supported.

Time.parse("Thu Nov 29 14:33:20 GMT 2001")
# => 2001-11-29 14:33:20 +0000

Time.parse("2011-10-05T22:26:12-04:00")
# => 2011-10-05 22:26:12 -0400

But too powerful, sometimes.

Time.parse("Thu Nov 29 a<i class="emoji" title=":b:"><img class="emoji" src="https://twemoji.maxcdn.com/36x36/1f171.png" /></i>c GMT 2001")
# 2017-11-29 00:00:00 +0100

To understand why it is we need to look at the documentation of Time.parse. There actually exists the second argument, a relative date which Ruby could fallback to when any part of the given string could not be parsed, with Time.now as the default date time.

This is a haunting feature. Apparently for the example above, the date string is wrong. Let me repeat, IT IS WRONG, and we should have an exception raised rather than a wrong date returned.

More confusing examples of Time.parse.

Time.parse("12/27") # => 2017-12-27 00:00:00 +0100
Time.parse("27/12/2017") # => 2017-12-27 00:00:00 +0100

6. Delegator

Let’s see this example.

require "delegate"

class Foo < Delegator
  def initialize(the_obj)
    @the_obj = the_obj
  end

  def __getobj__
    @the_obj
  end
end

foo = Foo.new(1)
foo.inspect # => 1
foo == 1 # => true

In my humble opinion, this behavior is completely WRONG and is giving a lot of false assumption because a delegator of 1 is absolutely not equal to 1.

Equality — At the Object level, == returns true only if obj and other are the same object. - Ruby documentation.

Obviously they are not the same object.

The reason behind this behavior is that for each message received, the delegator will just dumbly pass it to the delegated, no matter the message is hello or ==.

This leads to another problem.

foo = Foo.new(nil)
foo.inspect # => nil

When we try to dump the object foo, it dumps the delegated object instead of the object we want to dump. Ideally it could return something like.

foo = Foo.new(nil)
foo.inspect # <Foo: delegated: nil>

In case you are wondering why not opening a PR but writing an article and acting like a dick, yes I did. I submitted a ticket and also a pull request, but no feedback from the Ruby team unfortunately, hope I could get one soon.


NGUY HIỂM! KHU VỰC NHIỀU GIÓ!
Khuyến cáo giữ chặt bàn phím và lướt thật nhanh khi đi qua khu vực này.
Chức năng này hỗ trợ markdown và các thứ liên quan.

Bài viết cùng chủ đề

Euruko 2017 Notes

Vừa rồi mình đi Euruko 2017 ở Budapest, một số bài nói cũng khá thú vị nên mình sẽ note lại ở đây.

Bundler Gotcha

A few days ago I encountered a strange behavior of Bundler so this post notes down how my experience with it was.

Five Rails Gotchas

It’s undeniable that Rails is a great framework to speedily build up your application. However, despite of its handiness, like other frameworks, Rails has its own flaws and is never a silver bullet. This post is going to show you some of the gotchas (or pitfalls you name it) I encountered while working with Rails.