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.com/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.
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
What? Why do those two lines return different values – they are both looking up the same
Let’s trace it.
CONST inside of class
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
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
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
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 # 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
class Outer::Shortcut # everything here is part of Shortcut end
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
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.