Hacker News

What I didn't know about Ruby Classes(dumas-olivier.medium.com)

86 pointsolivdums posted 10 days ago30 Comments
echelon said 9 days ago:

Ruby's flexibility makes it great for going fast and hacking things, but oh my god can it lead to monolithic spaghetti.

Magical eager backfills, mysterious action at a distance method_missing dispatch, crazy class hierarchies and transclusion, endpoints with reflective behavior, nonsensical tests with horrific abuse of rspec, ...

I've spent engineering years tearing down Ruby crud others have built, and it's led me to have a distaste for the language like no other.

nerdwaller said 9 days ago:

What’s crazy to me is that despite much of this and my general dislike of ruby, I still often reach for rails to get an idea up and out. For my uses I’d rather just test something out and either let it burn down quickly or pay the price eventually to fix it (an old colleague called this a “champagne problem”). I’ve only had to pay the price a few times, which probably made each sequential one easier to avoid some parts.

Rails is still a phenomenal framework to use, again something I somewhat begrudgingly admit.

rantwasp said 8 days ago:

with great power comes great responsibility.

ruby is amazing and once you get past your keanu stage and learn some of its most common pitfalls it's insanely good. it's one of the few programming languages that brings me joy when i use it.

also as a side note: if you believe rails==ruby you are limiting yourself. ruby is so much more than rails

brundolf said 8 days ago:

It's interesting, there seem to be two primary camps when people talk about technologies that "bring them joy".

One camp feels this way about maximally-dynamic environments like Lisp and Ruby, which presumably provide "the highest bandwidth" when it comes to translating ideas into code.

Whereas for me and others, it's the exact opposite: these languages create nothing but anxiety. Joyful programming, for me, is programming where I can feel confident and peaceful knowing that every possible contingency of a given piece of code has been either accounted-for or prevented. Knowing that I'm not going to be blindsided by anything, being able to narrow my focus to the problem at hand instead of being overwhelmed with the infinite number of things that could go wrong.

I wonder what causes such a divergence in mindset

rantwasp said 8 days ago:

i think safety (perceived or real) is different from joy. I also think it’s really hard to enjoy something repetitive and super verbose.

pmontra said 8 days ago:

Yes, some developers like to be needlessly clever and do in Ruby things that they would never do in PHP or Java. I also lost time deciphering some clever metaprogramming.

It is more about the developer than the language though. I go with simple solutions no matter which language I use. I worked on some old code of mine last week (it was Python) and I couldn't understand it immediately. I fixed a problem and I'll rewrite part of it this week to make it clear what it does. It will also lower the chances of bugs.

herbst said 8 days ago:

I love creating nearly everything with rails, however i agree picking and cleaning an foreign codebase has always been a pain when i didnt know the team/dev follows a very similar pattern

anonunivgrad said 9 days ago:

It is a lovely language to use but it provides a few footguns too many. The main problem is that the community suffers from bad taste.

said 9 days ago:
briandear said 8 days ago:

The community is the reason Ruby is amazing. And Matz is one of the nicest guys you’ll ever meet. So genius, yet so humble.

anonunivgrad said 8 days ago:

The language and standard library are great. I mean that there's a tendency in the third-party libraries to abuse the features of the language for the sake of cute syntax and in the process create a difficult to understand beast where a more straightforward library would do.

dragonwriter said 9 days ago:

> Classes are constants

No, classes are objects.

The class declaration syntax assigns the class it creates to the constant given as the name, but it's quite possible to create a class without assigning it to a constant (e.g., via Class.new).

Lammy said 9 days ago:

> Include and Extend your Classes

And — since Ruby 2.0 — Module#prepend, which places the prepended Module ahead of the caller in the ancestor chain https://ruby-doc.org/core/Module.html#method-i-prepend

amarshall said 8 days ago:

Which is a much nicer alternative to `alias_method_chain` and similar for monkey-patching or enhancing an existing method in a class as it makes more clear the existence and origin of the patch.

burlesona said 8 days ago:

I love watching people go deep on Ruby and start to understand how it works under the hood. It’s a beautiful and very powerful language.

I will say one thing, though. When you learn how to use all of the dynamic dispatch, meta-programming etc. it can sound like fun to find places to do this stuff in prod.

I think it’s better to recognize that all of Ruby exists at runtime, all the time. A lot of the really nifty stuff in Ruby is best thought of as debugging and testing tools. Some parts may be appropriate for framework or library code, but only in very specific situations. Production code should normally be very vanilla and use only the core of the language.

It’s true that most other languages don’t offer you this power, and if you don’t have it you can’t abuse it. And that can be a good thing too.

Ruby is a language full of power tools and very sharp knives. It trusts you to make your own decisions with that power, and doesn’t protect you from yourself. When you understand that, you can approach it with care and have a uniquely wonderful programming experience.

baron816 said 9 days ago:

I don’t think I really understood Ruby classes until I learned JavaScript (which didn’t really have classes at the time, and are still pretty different). The mental model that developed for me was that classes are basically fancy functions that create objects.

riffraff said 8 days ago:

> The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said "Master, I have heard that objects are a very good thing - is this true?" Qc Na looked pityingly at his student and replied, "Foolish pupil - objects are merely a poor man's closures."

> Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire "Lambda: The Ultimate..." series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress. On his next walk with Qc Na, Anton attempted to impress his master by saying "Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures." Qc Na responded by hitting Anton with his stick, saying "When will you learn? Closures are a poor man's object." At that moment, Anton became enlightened.


QuesnayJr said 8 days ago:

i recognize this is a parody of a Zen koan, but I swear to God, it is completely true.

anonunivgrad said 9 days ago:

There's also eigenclasses to further complicate the object model.

sergeykish said 8 days ago:

V8 JavaScript has hidden classes; Ruby makes it visible and it is standard.

JadeNB said 8 days ago:

What’s an eigenclass?

inopinatus said 8 days ago:

The language mostly refers to them as "singleton classes", because logically speaking there is exactly one of them for every object.

Every Ruby object is an instance of its singleton class. Even when it appears to be an instance of, say, "File" or "Hash", their true individual identity is their singleton. Hence with:

    class Foo; end
    foo = Foo.new

    foo.singleton_class            #=> #<Class:#<Foo:0x00007ff6cc195ee0>>
    foo.singleton_class < Foo      #=> true
    foo.is_a?(foo.singleton_class) #=> true
    foo.singleton_class.ancestors  #=> [#<Class:#<Foo:0x00007ff6cc195ee0>>, Foo, Object, Kernel, BasicObject]
Most significantly, this class is where any per-object method is actually contained, hence:

    class Foo
      def self.hello

    Foo.hello #=> 42
    Foo.singleton_class.instance_methods(false) #=> [:hello]
There are three things worth observing that, once fully absorbed, helped me understand all this more instinctively:

1. All Ruby methods are the instance methods of a class.

2. What we call class methods, such as Foo.hello above, are technically instance methods of the singleton class of a Class object. But that's something of a mouthful, so we say class method instead.

3. Extending an object with a module is, by definition, including that module in the ancestors of its singleton.

sergeykish said 8 days ago:

1. As instance methods always defined in some class:

    foo = Object.new
    def foo.hello
    foo.method(:hello).owner == foo.singleton_class
and class definition is just a syntactic sugar:

    Foo = Class.new
    def Foo.hello

    Foo.method(:hello).owner == Foo.singleton_class
2. Class method is just a method of objects class

    f = Foo.new
inopinatus said 8 days ago:

It's dangerous to assume that definition by keyword is syntactic sugar, because there are crucial lexical differences that will bite the novice metaprogrammer on the backside. For example, writing

    class Foo; end
differs from

    Foo = Class.new
since the former will open an existing class, whilst the latter will overwrite the constant with a new class, and then the cref (roughly speaking, constant search path) is different due to the module nesting structure; hence:

    class Foo

      def self.hello1

    def Foo.hello2

    Foo.hello1 #=> 42
    Foo.hello2 #=> NameError: uninitialized constant MAGIC_NUMBER
and worse:

    Bar = Class.new do
      MAGIC_NUMBER = 99
defines MAGIC_NUMBER as a top-level constant, not as Bar::MAGIC_NUMBER, which is bad enough in itself, but now

    Foo.hello2 #=> 99
which is the kind of subtle misbehaviour that drives people nuts trying to resolve.

Without going into the arcane detail, there are similarly subtle variations that'll show up, involving the default definee (aka the third implicit context), and closures contained in the class definitions.

So I'm very sparing in my use of Class.new and even Module.new, I'll restrict them to carefully written framework methods.

> Class method is just a method of objects class

You'd hope. But look at the mechanics of Kernel#class. Objects don't work from a reference to their (apparent) class, the obj->klass pointer doesn't necessarily go there; it references the head of the linked list of all ancestors, and if you've referred to the singleton in any fashion it'll point to that (absent funny business like prepending the singleton). Then rb_class_real has to iterate along the list skipping the singleton and any ICLASS entries, and assumes the first thing it sees otherwise is the class you meant.

The point being that an object's apparent class is defined by the first object in its ancestors list that isn't its singleton class or a included/prepended module. In theory, this should be invariant across the object's lifetime. In practice, Ruby recomputes it each time. The reason for this is that as soon as you reference the singleton of an object, Ruby a) allocates it, and b) updates the obj->klass pointer to be the singleton, not the class it was made from.

Also, you can screw with people's assumptions via def foo.class; Object; end, which just demonstrates how wilfully ignoring the Law of Demeter gets you into trouble.

ljm said 8 days ago:

It's also what gives you this funky syntax:

    class Foo
      class << self
        def bar; end
tomstuart said 8 days ago:

It’s a class dedicated to a single object. Every object has its own eigenclass — this sounds expensive but in practice they’re lazily created. Regular classes are used to store the definitions of methods which are available on all instances of that class. In contrast, a single object’s eigenclass is used to store the definitions of methods which are available on that object only.

`def foo.bar … end` defines a `bar` method in the eigenclass of the object `foo`, which can then be called with `foo.bar`. (This is how “class methods” work in Ruby: their definitions are stored in the class object’s eigenclass so that those methods can be called only on that specific class rather than all classes.)

marcandre said 8 days ago:

In Ruby they are called "singleton classes", but the actual technical term is metaclass: https://en.wikipedia.org/wiki/Metaclass (check the Ruby entry)

masklinn said 8 days ago:

> the actual technical term is metaclass

Hard disagree. metaclass normally is the class of a class (aka `Class` in ruby), and is used to work manipulate classes.

In Ruby, a metaclass is instead an implicit per-object class.

Lammy said 8 days ago:

It's like a "shadow class" that's unique to an instance of something. No written description of this concept ever really 'clicked' for me, personally, so maybe an example will help. Here's a class with a single method `:hello` that will simultaneously increment a counter on its class and on its singleton_class/eigenclass/metaclass:

  irb:1* my_class = Class.new do
  irb:2*   def hello
  irb:2*     "I've counted #{self.class.instance_variable_set(:@count, (self.class.instance_variable_get(:@count) || 0) + 1)} of #{self.class}… " +
  irb:2*     "but only #{self.singleton_class.instance_variable_set(:@count, (self.singleton_class.instance_variable_get(:@count) || 0) + 1)} of #{self.singleton_class}!"
  irb:1*   end
  irb:0> end
  irb:0> foo = my_class.new
  irb:0> bar = my_class.new

Then if we send :hello a few times to each instance you can see how it behaves:

  irb:0> foo.hello
  => "I've counted 1 of #<Class:0x0000561efb9d8238>… but only 1 of #<Class:#<#<Class:0x0000561efb9d8238>:0x0000561efbaa8488>>!"
  irb:0> bar.hello
  => "I've counted 2 of #<Class:0x0000561efb9d8238>… but only 1 of #<Class:#<#<Class:0x0000561efb9d8238>:0x0000561efb9d7040>>!"

  irb:0> foo.hello
  => "I've counted 3 of #<Class:0x0000561efb9d8238>… but only 2 of #<Class:#<#<Class:0x0000561efb9d8238>:0x0000561efbaa8488>>!"
  irb:0> bar.hello
  => "I've counted 4 of #<Class:0x0000561efb9d8238>… but only 2 of #<Class:#<#<Class:0x0000561efb9d8238>:0x0000561efb9d7040>>!"

  irb:0> foo.hello
  => "I've counted 5 of #<Class:0x0000561efb9d8238>… but only 3 of #<Class:#<#<Class:0x0000561efb9d8238>:0x0000561efbaa8488>>!"
  irb:0> foo.hello
  => "I've counted 6 of #<Class:0x0000561efb9d8238>… but only 4 of #<Class:#<#<Class:0x0000561efb9d8238>:0x0000561efbaa8488>>!"
  irb:0> foo.hello
  => "I've counted 7 of #<Class:0x0000561efb9d8238>… but only 5 of #<Class:#<#<Class:0x0000561efb9d8238>:0x0000561efbaa8488>>!"
  irb:0> bar.hello
  => "I've counted 8 of #<Class:0x0000561efb9d8238>… but only 3 of #<Class:#<#<Class:0x0000561efb9d8238>:0x0000561efb9d7040>>!"

For a real-world example of how this can be useful, I use this pattern in my Jekyll multimedia toolbox to handle the specifics of any certain type of media file (e.g. images, videos, audio, etc). I defined separate Modules for separate media_type handling, a single instance will detect the media_type of its associated file (from the file extension or filemagic), then the instance will differentiate itself by Module#prepend-ing the media_type-specific Module to the instance's `singleton_class`. Then the next instance for the next possible-different-type media file has a clean undifferentiated base to start from and the process can repeat: https://github.com/okeeblow/DistorteD/blob/master/DistorteD-...
drosan said 8 days ago:

Nice paywalled article huh