Hacker News new | past | comments | ask | show | jobs | submit login
What I didn't know about Ruby Classes (dumas-olivier.medium.com)
86 points by olivdums 7 days ago | hide | past | favorite | 28 comments





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.


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.


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


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


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

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.


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

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.

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.

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.

> 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).


> 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


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.

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.


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.

> 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.

http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/m...


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

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

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

What’s an eigenclass?

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
then

    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
        42
      end
    end

    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.


1. As instance methods always defined in some class:

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

    Foo = Class.new
    def Foo.hello
      42
    end

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

    f = Foo.new
    f.class.hello

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
      MAGIC_NUMBER=42

      def self.hello1
        MAGIC_NUMBER
      end
    end

    def Foo.hello2
      MAGIC_NUMBER
    end

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

    Bar = Class.new do
      MAGIC_NUMBER = 99
    end
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.


It's also what gives you this funky syntax:

    class Foo
      class << self
        def bar; end
      end
    end

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.)


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

> 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.


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-...



Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: