An introduction to metaprogramming in Ruby
blog.appsignal.com
blog.appsignal.com
I don't like it in python, I don't like it in Java.
But e.g. in Rails, sure I bump my head sometimes, but overall I like how magical Rails feels because it lets me go so fast.
Maybe it's because Ruby alone is willing to sacrifice so much speed (though not THAT much vs python tbh) and is willing to go all-in on it, that they enable magic to be so deeply magical that it can deliver adequate value to compensate for being more inscrutable?
Whereas other languages' metaprogramming systems keep you a little more leashed.
On the other hand, that deep magic metaprogramming can be really hard to follow if you need to understand how it works. Tracing back through (or even debugging) a metaprogramming-heavy codebase is a nightmare.
I'd argue that deep magic metaprogramming is great for when you have abstractions you almost never need to dig into. Rails is great because it's relatively rare that I need to go spelunking in the rails codebase itself (and thus understand the deep magic). Instead, I can rely on a huge pile of documentation, stack overflow answers, conference talks, etc to figure out how to use rails' abstractions (like `has_one :posts`) without needing to understand their implementation.
On the other hand, the average production codebase should minimize their use of metaprogramming. When I don't understand how Joe's `Whatsit` interface works, I'm much more likely to need to dig into Joe's code to understand how to use that abstraction. If I have to understand Joe's deep magic every time I do that, it's a net loss.
This lack of easy debugging capabilities has cultivated a whole group of developers whose primary way of debugging is 'let me out in a print statement'.
I have never looked back.
Jard is a tool that is simply done/complete/ready.
JavaScript as a developer culture seems to have convinced a lot of folks that if a package isn't updated at least a few times a year, it's "dead".
RubyJard looks cool, but the repo is marked as "Archived" and the last commit predates Ruby 3. And, in fact, it looks like there are issues with Ruby 3.2 and RubyJard.
Most software cannot really ever be "done", if only because no software operates in a vacuum. Nearly all software relies on a language, a standard library, an OS, direct dependencies, indirect dependencies, etc. The only way your software can be "done" is if it relies on absolutely nothing, and very little useful software does that.
Well, that sucks. Still, I urge people to clone a copy, update the pry dependency and give it a try because I've never run into any issues and it's at the center of my debug game. I'd be devastated to lose it.
Agree with everything you said
(disclaimer: I do rails all day)
One thing I've noticed, and this is something that seems to bother non-Ruby engineers, is the people that flourish in a complex Ruby application prefer to read code over documentation.
You hit the nail on the head.
Is it the most maintainable code? No. Is it the easiest to understand? It depends tbh. Rails provides so much out-of-the-box stuff that lots of rails apps end up looking sufficiently similar, especially if they're small-to-medium-sized. But yes you can end up writing some dreadful spaghetti without a bit of discipline, but imo that's true of python and (node) js as well.
But damn is it SO fast to get up and going. And given product-market fit is why most projects and products fail, it's hard to argue against using Rails for me, since if I'm even still here to complain about it later, that's already a win.
I know this feeling!
If one using Rails accepts this and tries not to fight it, then the metaprogramming of Rails provides a lot of goodies.
I prefer convention over configuration because once learned it just works. For me in Rails when I look at an URL, I kinda know what is the file that handles that (the controller). And that is what I appreciate.
Of course, different needs require different solutions. I also worked with Sinatra which does not have this. But I choose it exactly because it is light and it allows me to group my logic in a different way.
Which is still plenty fast for CRUD web applications even at large scales, don’t get me wrong, I’m just pointing out that JS for example often has a 10x multiplier between it and Ruby/Python.
Python is the de facto glue language.
You going "fast" now means that others (including the future you) will go "slow" later.
I've also worked on Java, Go, Elixir, and Haskell codebases.
I find that anytime we went "Slow" was because of decisions that were made around the data model, which can happen in any framework or language.
The thing that I love about working in Ruby is Rubyists tend to write fantastic tests (which double as documentation of what ever business logic), and the culture around "convention over configuration."
So even when we decided to port one of those Rails apps to Elixir/Phoenix, it wasn't an absolutely awful rewrite.
It's fine for those 2-3 cases, and should be banned everywhere else. In rails is highly abused. What rails achieved and it's good at can be achieved with less metaprogramming, but of course nobody is going to build another rails, since rails is already there.
Metaprogramming is just one of the things that make Ruby a joy to use. It's not really a reason unto itself.
I'm sure there are rare cases where these techniques are useful. Like creating developer tools or making your own object persistence layer or winning a code golf contest. But if you're an app developer for goodness sake just write a few extra lines of code. Do whatever it is you're doing the verbose and clear way, not the slightly shorter and super obtuse way.
Stuffing method definitions into classes at runtime, monkey patching, dynamically generating method calls with .send. These will all be very puzzling for any future developer that works on your code, senior or junior. And come with bunches of technical pitfalls. Writing clear and maintainable code is a higher calling for us than reducing LOC and showcasing neat tricks. Even if you call yourself a Rubyist. Speaking from experience.
"Your honey, I admit to killing him, and I'm sorry, but in my defense, he monkey patched a core method"
"Case dismissed!"
rule :name, [:param] => [:dep1, 'dep2'] do |t|
where every argument except the name can either be missing, single (value) or multiple (array). Sure, it has the "advantage" that it's syntactically valid Ruby code, but it then requires some 70 lines of awful code to actually parse that data into a usable construct: https://github.com/ruby/rake/blob/7b50e9dc37abc57fd365c16cb1...Even `send` is frowned upon, but allowed under some circumstances.
However it was every startups default choice for a while, and those code bases get wrecked and have no oversight until way later. A lot of bad ideas becomes foundational. This scenario is far more common than good Ruby code bases. It’s just easier to rule out Ruby when looking for new jobs.
The book "Eloquent Ruby" has several chapters on how to use "method_missing". None of those chapters say the only valid use of method_missing: "Never use it."
Lots of languages have deep flaws. Javascript, Python, Java, Perl, C obviously, all let you do some pretty horrific stuff if you really want to. Ruby is the only language where the horror is embraced by the ecosystem and its users.
There's a reason most engineers are adamant you shouldn't use this. It's why learning Ruby as your first language is probably a bad idea, since it normalizes what every other language has agreed is bad practice.
Don't!
Everyone's mileage may vary but i definitelly do recommend learnign Ruby as a first leanguage. What i also recommend starting developers is to not follow dogmatic advices blindly.
As with all abstraction mechanisms, you avoid them until you need them. When you do need them, they become a force multiplier.
A lot of macros can be avoided with data driven programming, which is likely one of the strongest techniques in terms of cost/benefit.
A light form of meta programming is source code generation (often used in tandem with data driven). It lets you have macro-like power, but the guts are spilled out for you to modify further or reason about easily. In some languages it’s the only thing you can do at that abstraction level.
In any case, meta programming is very powerful. But its quality and utility hinges on you to avoid it until you exhaust all of the lower level techniques. Else you end up with the worst kind of wrong abstraction.
Don't! Be dogmatic about programming. There are always use cases.
class Wrapper < BasicObject
def initialize(obj)
@obj = obj
end
def method_missing(name, *, **, &)
::Kernel.puts "Calling #{name}"
@obj.send(name, *, **, &)
end
endRuby does not have to do what other languages are doing. There is no good or bad.
Do you have any stories? Tried using it and it backfired? Were bitten by a nasty bug in a gem?
We are turning to https://sorbet.org/ to reign in the madness. I'm keen to know if others are doing the same, and how they are finding it (pros and cons)
Metaprogramming is just a higher level of abstraction that lets you express things that would otherwise be tedious or repetitive. It’s not that different than using a loop to avoid having to write the same thing over and over again. Avoiding it at all costs is dogmatic and kind of silly.
The single most difficult thing in software development is the fact that the system doesn't exist, it is defined by mystical incantations, and we work on that invisible system by changing the instructions to build it.
Anything that makes the understanding or "building" that system mentally is no-go.
Flashback to one of my favorite debugging stories where an entire java code-base was meta-programmed into existance at runtime and would constantly throw errors to lines of files that didn't exist.
Broad proscriptions such as this almost always set my teeth in edge. Sure, meta programming can be a foot gun. But it can also be a way to cleanly achieve specific goals. Metaprogramming should by no means be the first tool reached for, but throwing out the baby with the bath water is an overcorrection.
It also means you are losing out on a significant portion of what makes Ruby Ruby.
Oh no anyway.
Pretending Rails sucks never gets old, I guess.
I've worked for large companies and startups, and seen rails several times, along with .net, java, scala, php, node and other backbends.
The only thing that mattered in any of their technical successes was the quality of the engineers working on them.
There were certainly minor differences- irritations like compile times or dynamic typing- but the trade offs were 90% of the time personal preference, when compared to the difference made by the strengths of the actual people writing code.
That >75% value created by companies which used Ruby did not create an equivalent debt in another column.
If Ruby-first companies that aren't AirBnb (for example) fail, it's not because AirBnb succeeded. (Perhaps unless your company was called Couch Surfing.)
To protest on the grounds that for some teams, Ruby might be the wrong choice is to willfully ignore the key detail about the whole outlier-degree success thing.
Lombok?
I mean, that's fair, but if you're using it in a team based environment, eventually someone's going to misuse it and you'll have to figure out what happened. I think how bad things go when you misuse it is a valid consideration, even if it shouldn't dominate everything else.
> It gets way more hate than it actually deserves. Spring does 100000000x more in terms of making code based confusing to navigate and confusing errors yet receives very little criticism from the Java community.
I mean, I don't think you're wrong, but I'm also not in the java community. The few times I've forayed into there, it seemed more like a dynamically-typed (or more accurately, stringly-typed using only the names of config) annotation language rather than java. And if I wanted string-based typing and confusing semantics, I'd just write my whole project in bash.
Spring may be the larger monstrosity, but they are both horrible ideas.
The ideas, however, are solid. They are just implemented better in other frameworks such as https://immutables.github.io/, using officially supposed JVM tools like annotation processors.
With python I knew I would end up over engineering with meta classes, magic methods and what not. Same with ruby.
Using send is extremely common especially when mocking private methods in rspec. I guess I am speaking from a rails lense, but what other lense is there for ruby development?
I am forever stuck in a world where "articles" aren't something you read in less than a minute.
The listed "solutions" are also more fun. At least one metaprograms the test harness itself. Is that "cheating"? Hm.
function process_item(item, action)
{
item[action]();
}
(Also similarly forgetting the guard statement).Probably not great code, but still.
Since there are no classes in JS and everything is an object, you technically have complete control over such 'classes' and 'methods'. This is probably why people keep trying on turning JS into a different and saner language, or better yet, are using another language entirely like TS. Which is a shame, because it's kind of fun to dynamically generate new functions on the fly and having complete freedom to do anything you want.
I believe this is the same in ruby.
Both are heavily influenced by Smalltalk.