16

How Ruby Can Surprise You

 4 years ago
source link: https://www.tuicool.com/articles/BzU7FzQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

In the Showmax Backend team, Ruby is our preferred language. So, when we interview people for jobs and they say they know Ruby, we tend to chat about its quirks and specifics, and sometimes we put the candidate under a bit of pressure.

Here, we picked some interview questions that we ask that tend be either unknown, or answered incorrectly. For this blog post, as you may have guessed, some knowledge of Ruby is expected.

If you are considering applying to join Showmax Engineering, don’t fret, these are the hardest ones we ask. Really, just finishing this article would make a really good first impression!

super vs super()

Let’s start with something easy - one that is (probably) known by every non-beginner Rubyist. Ruby is unique among mainstream languages in many aspects, one of them is that parentheses for method calls are optional:

[1] pry(main)> def foo(*args); pp(args); end
=> :foo
[2] pry(main)> foo(1, 2, 3)
[1, 2, 3]
=> [1, 2, 3]
[3] pry(main)> foo 1, 2, 3
[1, 2, 3]
=> [1, 2, 3]
[4] pry(main)> foo()
[]
=> []
[5] pry(main)> foo
[]
=> []

This is something every Ruby developer knows. However, as they say, there is an exception to every rule.

[1] pry(main)> class Account
[1] pry(main)*   def initialize(*args)
[1] pry(main)*     pp(args)
[1] pry(main)*   end
[1] pry(main)* end
=> :initialize
[2] pry(main)> class User < Account
[2] pry(main)*   def initialize(a, b)
[2] pry(main)*     super()
[2] pry(main)*   end
[2] pry(main)* end
=> :initialize
[3] pry(main)> class Guest < Account
[3] pry(main)*   def initialize(a, b)
[3] pry(main)*     super
[3] pry(main)*   end
[3] pry(main)* end
=> :initialize
[4] pry(main)> User.new(1, 2)
[]
=> #<User:0x00005621248cfcf8>
[5] pry(main)> Guest.new(1, 2)
[1, 2]
=> #<Guest:0x00005621248fb650>

As you can see, super and super() do not behave in the same way. super forwards all arguments passed to the initialize method, while super() calls base class’ initialize without any arguments. It’s a nice example of inconsistency right in the core of the language.

I know that super is actually a keyword and not a method, but the usage is the same nevertheless and can confuse some people.

Two Kinds of Blocks

In Ruby, we have two ways to specify blocks, do; end and { } :

[1] pry(main)> def foo; puts(yield); end
=> :foo
[2] pry(main)> foo { 1 }
1
=> nil
[3] pry(main)> foo do
[3] pry(main)*   1
[3] pry(main)* end
1
=> nil

The question is, how are these two ways different? A surprising amount of people (even long time Rubyists) will tell you there is no real difference, and that it comes down to just aesthetic, abiding simple rule: “ { } for single line blocks, do; end for multi line blocks”.

But it’s not so simple. At first glance, the blocks do indeed behave the same way. But, what is not obvious is that they have different precedences. This problem is amplified by parentheses for method calls being optional, and any method being able to take a block without error - even when it has no use for it. It’s not always obvious which block belongs to which method.

Compare:

[1] pry(main)> puts [1, 2].any? { |i| i == 3 }
false
=> nil
[2] pry(main)> puts [1, 2].any? do |i|; i == 3; end
true
=> nil

To put it simply, in puts foo { } the {} binds to foo , and in puts foo do; end the do; end binds to the puts . It can, of course, be solved by parentheses:

[1] pry(main)> puts([1, 2].any? do |i|; i == 3; end)
false
=> nil

This is rarely a problem, but when it does cause a bug, some people are really surprised that they can’t just pick the block type they prefer.

WTF is eigenclass?

Eigenclass is sometimes called singleton class, but do not confuse it with the singleton design pattern. In Ruby, everything is an object (yes, even things like 1 ) - even classes are just instances of the Class class:

[1] pry(main)> String.class
=> Class
[2] pry(main)> String.new.class
=> String

Every instance has its own chain of ancestors that are used for looking up methods during method dispatch. This implies some interesting properties. For example, if String is just an instance of Class , where are class methods of String defined?

[1] pry(main)> String.singleton_methods(false)
=> [:try_convert]

Turns out that there is an anonymous class nested right above the instance, even before its base class. That class is called eigenclass, which has some interesting properties. I can define the method on a single instance of a class:

[1] pry(main)> a = ""
=> ""
[2] pry(main)> b = ""
=> ""
[3] pry(main)> class << b
[3] pry(main)*   def frozen?
[3] pry(main)*     'maybe?'
[3] pry(main)*   end
[3] pry(main)* end
=> :frozen?
[4] pry(main)> a.frozen?
=> false
[5] pry(main)> b.frozen?
=> "maybe?"

Notice that the #frozen? method is overridden just on b . A more common example of defining a singleton method is defining class methods:

[1] pry(main)> class << String
[1] pry(main)*   def foo
[1] pry(main)*     :foo
[1] pry(main)*   end
[1] pry(main)* end
=> :foo
[2] pry(main)> String.foo
=> :foo

Or the other common syntax:

[1] pry(main)> class String
[1] pry(main)*   def self.foo
[1] pry(main)*     :foo
[1] pry(main)*   end
[1] pry(main)* end
=> :foo
[2] pry(main)> String.foo
=> :foo

Natural Language in Ruby

This one is not actually on our interview questions list, but I’ve run into it on the interwebs and I consider it interesting. When I first saw this…

[1] pry(main)> How many sane ways are there to use Ruby rescue methods.
[1] pry(main)* none?
=> false

…I admit I was surprised that this is actually valid Ruby code. It just looks a little too weird for this to be actual code. But, breaking it down reveals why it works. Remove the rescue keyword and you get:

[2] pry(main)> How many sane ways are there to use Ruby
NameError: uninitialized constant Ruby
Did you mean?  RubyVM
from (pry):3:in `__pry__'

rescue rescues (hm, what else it could do…) the NameError and calls methods.none? since the . at the end of line are merged.

Hm, makes sense now. But I still think it’s weird. :)

Credit where credit is due, the original source for this one is here at Rubywtf .

Finding Where Methods are Defined

Sometimes you have a method but are not sure where it is actually defined. I don’t really know if IDEs like RubyMine help with this, but for us vim and command line users, there is a convenient way to get the location of a method declaration. For example, if I want to know where SecureRandom.hex is defined, I can do:

[1] pry(main)> require 'securerandom'
=> true
[2] pry(main)> SecureRandom.method(:hex).source_location
=> ["/home/user/.rbenv/versions/2.6.5/lib/Ruby/2.6.0/securerandom.rb", 158]

There it is in the file on line 158. This is especially useful when combined with a debugging session in pry. Of course, if you are using pry anyways, there is an edit command that allows you to redefine any method.

If you just want to check where a method is defined or you only have irb, #source_location is your friend. The caveat is that it only works for methods defined in Ruby. You can’t use it for methods written in C (which means lots of core methods).

And, Ruby 2.7 will also have #const_source_location for location constants .

Interaction Between Private Methods and #method_missing

I ran into this one while debugging an issue in our in-house logger interface, which is basically a wrapper around Log4r that uses something like:

def method_missing(method, *args, &block)
  logger.respond_to?(method) ? logger.send(method, *args, &block) : super
  nil
end

Here’s the question: What does the method dispatch look like?

Let’s look at an example:

$ cat dispatch.rb
#!/usr/bin/env Ruby
# frozen_string_literal: true
$-v = true

class Account
  private def foo; end
  def baz; end
end

class User < Account
  private def baz; end
  def method_missing(m, *args)
    puts("User - method_missing")
    super
  end
end

[
  ->() { User.new.foo },
  ->() { User.new.bar },
  ->() { User.new.baz },
].each do |callback|
  callback.call
rescue => e
  puts(e.message.lines.first)
end

We get this output:

$ ./dispatch.rb
User - method_missing
private method `foo' called for #<User:0x000055555597b198>
User - method_missing
undefined method `bar' for #<User:0x00005555559783f8>
User - method_missing
private method `baz' called for #<User:0x0000555555981a70>

The thing I want you to notice here is that, despite going through the method_missing every time, the exception message is different the second time. The idea that calling a private method even triggers the #method_missing feels weird at first glance.

After digging into Ruby source code for BasicObject#method_missing , it becomes apparent that it can throw several different exception messages:

static void
raise_method_missing(rb_execution_context_t *ec, int argc, const VALUE *argv, VALUE obj,
		     enum method_missing_reason last_call_status)
{
    VALUE exc = rb_eNoMethodError;
    VALUE format = 0;

    if (UNLIKELY(argc == 0)) {
	rb_raise(rb_eArgError, "no method name given");
    }
    else if (UNLIKELY(!SYMBOL_P(argv[0]))) {
	const VALUE e = rb_eArgError; /* TODO: TypeError? */
	rb_raise(e, "method name must be a Symbol but %"PRIsVALUE" is given",
		 rb_obj_class(argv[0]));
    }

    stack_check(ec);

    if (last_call_status & MISSING_PRIVATE) {
	format = rb_fstring_lit("private method `%s' called for %s%s%s");
    }
    else if (last_call_status & MISSING_PROTECTED) {
	format = rb_fstring_lit("protected method `%s' called for %s%s%s");
    }
    else if (last_call_status & MISSING_VCALL) {
	format = rb_fstring_lit("undefined local variable or method `%s' for %s%s%s");
	exc = rb_eNameError;
    }
    else if (last_call_status & MISSING_SUPER) {
	format = rb_fstring_lit("super: no superclass method `%s' for %s%s%s");
    }

    {
	exc = rb_make_no_method_exception(exc, format, obj, argc, argv,
					  last_call_status & (MISSING_FCALL|MISSING_VCALL));
	if (!(last_call_status & MISSING_MISSING)) {
	    rb_vm_pop_cfunc_frame();
	}
	rb_exc_raise(exc);
    }
}

That means that the chain is climbed twice, once for the original method and once for #method_missing . The point of this is that, in order to get the correct behavior, this quote from BasicObject#method_missing is pretty important:

If it is decided that a particular method should not be handled, then super should be called, so that ancestors can pick up the missing method.

Just raising NoMethodError is not a valid substitute for calling the super .

Floats Are Hard

This one is not Ruby specific, but it showed up a few weeks back in our #Rubylang Slack channel, posted by surprised developer:

[1] pry(main)> 19.99.floor(2)
=> 19.98

The thing with floats is that there are simply some numbers that are impossible to express precisely. Each floating point number in IEEE 754 consists of sign bit, biased exponent, and a fraction (I really encourage you to read the wiki article).

Using this excellent floating point converter , we can see that our number of 19.99 is actually, 19.989999999999998436805981327779591083526611328125 (notice the Inexact checkbox). And, guess what happens when you #floor(2) it? You get 19.98 (which, again, is not exact. The real number is 19.980000000000000426325641456060111522674560546875 ).

You can also check this directly from Ruby:

[1] pry(main)> sprintf('%.50f', 19.99)
=> "19.98999999999999843680598132777959108352661132812500"
[2] pry(main)> sprintf('%.50f', 19.99.floor(2))
=> "19.98000000000000042632564145606011152267456054687500"

The lesson here is pretty simple - floats are hard. Also, don’t use them for money-related record keeping (seriously, please don’t).

Further reading: What Every Computer Scientist Should Know About Floating-Point Arithmetic

What Will This Code Output?

Finally, just a little test showing that doing a code review is not always easy without trying to run the code in question. The question: What will this code output?

ALLOWED_TARGETS = ["dresden", "paris", "vienna"]

def missile_launch_allowed(target, secret_key)
  allowed = true
  аllowed = false if secret_key != 1234
  allowed = false unless ALLOWED_TARGETS.include?(target)
  allowed
end

puts(missile_launch_allowed("dresden", 9999))

Conclusion

These are a sample of the problems we pose in job interviews, but they are drawn from real-world surprises that Ruby has thrown at us. Have you encountered something that surprised you during your Ruby development? Write us [email protected] or on @ShowmaxDevs to share your story.

Finally, one more brain teaser. This time, no solution is given, figure it out on your own and write us at the email above. And if you happen to be in Prague or Beroun you can stop by for coffee and we can talk about it :)

x = [false, *(1..7), nil, 9]
  .map(&:object_id)
  .map { |i| i / 2 }
  .select { |i| i % 2 }
  .sum
  .+(STDOUT.fileno)

What will be the value of x (under mri Ruby 2.6.5 to prevent any arguments)? Can you figure it out without running the code? If yes,let us know!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK