What I didn't know about Ruby Classes
dumas-olivier.medium.com
dumas-olivier.medium.com
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.
Rails is still a phenomenal framework to use, again something I somewhat begrudgingly admit.
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
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
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.
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).
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
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.
> 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...
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.
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-...`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.)
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.
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 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.
class Foo
class << self
def bar; end
end
end