Please stop calling it "magic"
Japanese translation of this post is available!
Recently I’ve read (informative and well-written) article called 7 Gems Which Will Make Your Rails Code Look Awesome, and my eye was caught with this phrase in the description of decent_exposure gem:
If you are not a big fan of using magic – this library is not for you. (emphasis is mine)
This “m-word” was used to describe library that uses metaprogramming: adds expose
method to
Rails controller class, that generates trivial #index
/#show
/#new
methods for simple CRUD
situations.
I honestly don’t want to offend the article’s author, it is just a cause to say some important things. Lately, it seems to be pretty spread in Ruby/Rails communities: to use “magic” and “metaprogramming” as a kind of wicked synonyms. So, I just want to ask politely yet firmly:
Please stop calling it “magic”
Why? Read below.
Disclaimer: for the sake of discussion, I am using made-up examples, not particularly based on any existing library. I just don’t want my points to be lost in “Oh, he is just blaming %libraryname%”, like it already happened in the past.
So…
What is magic?
According to Wikipedia,
Magic … is the use of rituals, symbols, actions, gestures, and language with the aim of utilizing supernatural forces.→
and
The supernatural … includes all that cannot be explained by the laws of nature.→
Applying this to programming, we can formulate that:
Magic in code is the situation when some behavior emerges, whose cause, even for experienced developer on target language is hard/impossible to trace.
This is not a perfect definition, so let’s clarify it with some semi-theoretical examples.
class LandCruiserJ200 < Car
def type
:suv
end
def engine
'4.5 L'
end
end
LandCruiserJ200.new.drive! # "wroom! wrrroooom!"
“Is it magic? I don’t see any #drive!
method here, but it magically works???”
Any sane developer would laugh.
No, silly, it is just inheritance. We can guess that #drive!
method
defined in parent Car
class, and can easily find this class’ and method’s code (and/or its docs,
including the case when it is from some external gem).
OK, one more.
class LandCruiserJ200
include Drivable
def type
:suv
end
def engine
'4.5 L'
end
end
LandCruiserJ200.new.drive! # "wroom! wrrroooom!"
Oh, how weird! No inheritance here, where did #drive!
method came from? Magic!
… Or just normal mixin, find Drivable
’s docs and it probably will explain how and why it is used,
and what #drive!
method does.
One more?
class LandCruiserJ200
drivable type: :suv, engine: '4.5 L'
end
LandCruiserJ200.new.drive! # "wroom! wrrroooom!"
Oh, that’s definitely a case of unexplainable forbidden witchcraft, mere human will never understand that, right?..
… Or probably drivable
is a method, defined for a Class
directly or by extending with some
module (which is not the best practice possible, yet still pretty easy to guess for any non-junior
Rubyist), and you probably can:
LandCruiserJ200.method(:drivable) # => #<Method: Class(Drivable)#drivable>
LandCruiserJ200.method(:drivable).source_location # => /some/gem/drivable/lib/drivable.rb:123
So, what is magic?
I’ll show you:
class LandCruiserJ200Car
end
car = LandCruiserJ200Car.new
# => #<Car model="Land Cruiser J200">
car.drive! # "wroom! wrrroooom!"
Imagine the code above works. Why?
If I need to deal with the fact, OK, I can guess that probably
in current scope something provides a convention by which… hm… Everything named ...Car
is
treated specially? Or everything from folder app/cars/
?.. Or, there is some YAML-config, where all
car classes are listed?
What exactly is responsible for attaching the behavior and where can I find its docs, if I need them? How do I debug it, if it fails to behave the way I expect?..
That’s an example of “supernatural” behavior: e.g. when something happens not being obviously related to natural forces of the language. …And is bad exactly because of this fact: your natural tools and intuitions stop working, you need just to remember entire spellbook to deal with magical code.
What’s not magic
What junior does not understand, is not magic
It is common to see nowadays statements like “this is too magical for less experienced developers to understand”. As Matz says (reciting from memory), “Ruby is complex language to make developer’s life simple”, not the other way round.
Your car’s engine and your computer’s processor are pretty complicated, too, but that doesn’t mean that they are “magical”. Neither that you shouldn’t use them and stick to “less magical” solutions.
Monkey-patching is not magic
Yep, monkey-patching is questionable. Monkey-patching of core classes is considered deadly sin by some. And so on.
But, when some people coming from another language, see 5.days
and call that “magic”, why
should we repeat their opinion?
It may be counter-intuitive for other language developers, but for Rubyists it is pretty obvious what’s going on, and how to understand where method came from and look for its docs/implementations.
And, as a side note, open classes (even core ones) is significant feature for Ruby evolution, allowing both experiment with new concepts and provide backward compatibility by demand, with gems like backports and polyfill.
Metaprogramming is not magic
And now, back to the beginning of this article :)
“Metaprogramming”, e.g. generation of code objects during code execution, is one of the most powerful Ruby features, and it is so because how natural it is for the language. And due to this naturality, it is one of the first and the best guesses for code design in most cases.
Your class needs to have several math operators, just delegating to internal values? You do
%i[+ - * /].each { |op| define_method(op) { ...
Several similar classes need some “settings” that declaratively define their behavior? You add simple DSL of class methods. And so on.
Yes, later you may redecide this due to performance reasons, or for the sake of documentability (though nowadays we have pretty decent tools for documenting metaprogrammed stuff), or because your homogenous code objects became not that homogenous…
But metaprogramming is just one of the tools in your drawer and pretty useful one.
It does not become unnatural just because somebody has called it so 100 times.
Conclusion
Why am I writing this?
Because “m-word” is harmful for the community. It becomes a bad label that used with too much freedom both from outside and inside the Ruby world, being attached randomly to practices and features one just doesn’t like or doesn’t understand. This situation leads to decay of ideas, when perfectly well-designed and natural libraries and language features become almost “prohibited”.
So, simply put:
Dear Rubyists! Please stop use “magic” as a synonym of “metaprogramming”!