Noel Rappin Writes Here

Better Know A Ruby Thing Bonus: Contestants and Nesting

Posted on November 14, 2023


Sorry for skipping a week or two – I was approving copyedits on the book that is now called Programming Ruby 3.3, because we now want to be proactive about the next release.

Coincidentally, the copyedit review does relate to this newsletter. I noticed a particular code sample as I was going through the book again, and it highlights a feature of Ruby’s constant lookup that I didn’t discuss last time.


Hi – we’ve gotten some comments that the code snippets don’t look good on Apple Mail in dark mode. Buttondown is working on this, but if you need to, you can also find this newsletter on the web at https://blog/2023/11/better-know-a-ruby-thing-bonus-contestants-and-nesting

If you like this and want to see more of it in your mailbox, you can sign up at http://buttondown.email/noelrap. If you really like this and would like to support it financially, thanks, and you can sign up for a Monthly Subscription for $3/month or an Annual Subscription for $30/year.

Also, you can buy Programming Ruby 3.3 in ebook from Pragmatic or as a pre-order from Amazon, coming soon.

Thanks!


What’d I Miss?

Here is the code sample.

CONST = "top level"

module Outer
  CONST = "outer"
end

module Outer
  class Inner
    def self.get_const
      CONST
    end
  end
end

class Outer::Shortcut
  def self.get_const
    CONST
  end
end

puts Outer::Inner.get_const
puts Outer::Shortcut.get_const

So, we have a module and two classes. One class is explicitly declared inside a re-opened declaration of the outer module. The other class is declared using the shortcut syntax rather than explicitly declaring the outer module.

The last two lines print out different results.

The next-to-last line, puts Outer::Inner.get_const, results in "outer", but the last line, puts Outer::Shortcut.get_const, results in "top level".

What? Why do those two lines return different values – they are both looking up the same CONST, right?

Let’s trace it.

Outer::Inner.get_const calls CONST inside of class Inner. Since CONST is not defined inside class Inner, Ruby goes hunting. The next place Ruby looks for constants is based on the nesting at the point of the call. Class Inner is nested inside module Outer, and since module Outer defines CONST = "outer", that’s the result that gets printed out. An interesting point here is that the Outer::CONST value gets used even though CONST is not defined in the part of Outer that Inner is nested within.

Moving on, to Outer::Shortcut.get_const. We start the same way, CONST isn’t defined inside class Shortcut either. The difference is that Ruby does not consider class Outer::Shortcut to be nested the way that explicitly declaring module Outer external to class Inner is considered nesting. You can verify this by inserting a puts Module.nesting call before the two references to CONST – the one inside Inner will include Outer, the one inside Shortcut will not. The nesting does not find a value, Ruby keeps moving outward, and eventually decides that CONST equals "top level".

I’m not a big fan of the style of blog headline that tries to guess what the reader knows, like “10 things about Ruby Modules that you didn’t know”, so I don’t want to say that this should surprise you, but it surprised me.

And by surprised me, I mean it surprised me on my second pass through the book. I didn’t write the original snippet, it’s from the previous edition – I edited it, but the import of the snippet didn’t stick in my head until I went back to it fresh from writing the last newsletter.

Why is Ruby behaving this way?

I’m not completely sure, but I’d hazard a guess that it’s more likely to be an emergent property of the way the Ruby interpreter works than it is a deliberate design decision on its own.

If the code says module Outer as its own statement followed by class Inner, then there is space for you to include other definitions as part of module Outer, so Ruby actually opens module Outer:

module Outer
	# Anything here is part of Outer

  class Inner
    # Stuff here is part of Inner
  end

  # This is also part of Outer.
end

If the code says class Outer::Shortcut, there’s no space to define anything as part of Outer, and so Ruby does not open module Outer:

class Outer::Shortcut
  # everything here is part of Shortcut
end

Not opening Outer in this context is probably a deliberate design choice, but the downstream affect of changing the way constant lookup behaves feels like a side-effect to me. That said, I have no special knowledge here.

Do I have a hot take?

I have a few kind of lukewarm takes.

The first is to avoid the class Outer::Shortcut form both in regular code and avoid RSpec.describe Outer::Shortcut in specs, instead use the full version:

module Outer
  class NoLongerAShortcut
  end
end

Or

module Outer
  RSpec.describe NoLongerAShortcut do
  end
end

I realize the RSpec one is a little counter-intuitive.

This is what I do in my own projects, which is learned behavior from the days when the shortcut syntax was dicey with Rails autocorrect, and I’ve never gone back. Even though it irks me to give up the two text columns and have everything more indented.

If I encounter the shortcut style in a code review, I tend to recommend not doing it, for the Rails reason, and now in the future for the weird-constant-behavior reason. I don’t normally make a big deal about it if the other coder likes it and understands the trade off.

My second lukewarm take is that it’s probably not a high priority but Ruby should probably behave such that the shortcut form acts as nesting for constant lookup. There may be some principled or performance reason that I’m missing – lots of odd things about Ruby have principled reasons. But on the metric of “how hard is this to explain to a newbie”, this is pretty hard to explain and might be worth cleaning up. It’d probably break some existing code, though, which the Ruby core team is understandably reluctant to do.

My third lukewarm take is about deep module nesting in general. This is going to be wishy-washy even by my standards, but I don’t think of deep nesting as particularly elegant or idiomatic in Ruby, while at the same time, I completely understand how you can get to something like User::Posts::Api::V1::CreatePost and I’m not sure I have a better solution. (My bias would be toward UserApi::V1::CreatePosts or even UserApiV1::CreatePosts, but I’m not convinced those are better, and if you wanted to talk me out of them, I’d fold like a paper airplane.) I put it in the “strict scrutiny” bucket, if I can borrow an ill-fitting legal term. There’s a case to be made that deep nesting is the right choice, but I don’t think of it as the best default option.

And that’s the module definition quirk that I missed the first time around. Back to a full newsletter next week, not sure yet which Ruby thing I’m going to do yet. Also coming up, a full post about the Pickaxe book, which I’ll time for the actual release.



Comments

comments powered by Disqus



Copyright 2024 Noel Rappin

All opinions and thoughts expressed or shared in this article or post are my own and are independent of and should not be attributed to my current employer, Chime Financial, Inc., or its subsidiaries.