After a year and a half working in other languages, I’m back to Ruby. And, one of the things that I was amazed at Clojure is how simple things were.
Back to Ruby, I’m really surprised on how people overcomplicate things.
There’s no perfect language, nor perfect community of languages. In Clojure, there are fewer abstractions, which is not exactly a good thing – if you, let’s say, just want to create a simple page to show data from a database, you’ll find it extremely tedious to do it in Clojure, where in Ruby/Rails it’s just a few lines of code away. Ruby’s moto is “programmer happiness”, and this reflects in every library that they write.
But this kind of higher abstraction pays off with time. At least where I live, working with Ruby means working with Rails almost all the time. There are no “big competitions” for Rails or ActiveRecord (Sequel is a close one, but at the time of this post, AR have 10 times more downloads than Sequel), and other web frameworks are mostly “Rails-like”. But what bugs me most is “magic”.
For instance, let’s see this simple fragment of code: user.name
. This just calls a method in object user
. But, if user
is an ActiveRecord object, this will, the first time it’s called, hit method_missing
, then hit the database to describe a table which have the same name as the class name, lowercased and converted to underscore, pick up the fields’ names, then create these methods in the metaclass between ActiveRecord::Base
and our current class, then call the method again, then mark the class as “initialized”.
All this to avoid writing something as simples as:
class User < ActiveRecord::Base fields :name, :birth_date, :address end
And other fields you’ll find on your table. Because of this simple trade-of, we lose visibility (we don’t know which fields our database have until you use it for the first time), predictability (the first hit in the database changes your code at runtime, and also it’s an unexpected query), adds a lot of complexity and let’s face it, it just don’t solves a problem that big…
But we know ActiveRecord is known to be “magical”, so let’s see another ORM: Sequel. A simple mapping in sequel is to inherit Sequel::Model
, and it suffers from the same problem as AR (only differing on when the describe query is called). But it gets worse:
class User < Sequel::Model(:users) end # or... dataset = DB[:users].where(status: active) class User < Sequel::Model(dataset) end
This creates a dataset (Sequel’s abstraction for a query) and then dynamically generates a class that is created specifically to be inherited. But again, this could be solved with a simple configuration on the class:
class User < Sequel::Model set_table_name :users end # or... class User < Sequel::Model dataset_opts { |ds| ds[:users].where(active: true) } end
It still have some kind of implicitly-passed parameters (on Clojure, for instance, you need to pass a “connection” to database for every query – on AR and Sequel it just “brings up one from the air”) but it’s way better (and let’s not even mention “ROM-rb” – it generates methods on classes based on what parameters you passed to the mapping of your database!)
And it’s not only limited to ORMs: let’s see Anima, a library that tries to bring immutable objects to Ruby:
class User include Anima.new(:name, :age) end user = User.new(name: "foo", age: 20) user.name # => "foo" user.with(name: "bar").name # => "bar"
So far, so good… at least, in theory. If you look at this code, you’ll see that:
1. Anima.new
dinamically generates a module to be included at User
class
2. This module overloads the constructor of your class (remember: we’re not inheriting anything here, it’s a mix-in!)
3. The constructor is strict: it’ll require you to pass all the parameters as a hashmap, not more, nor less
Again, we can solve it with metaprogramming, but with less magic:
class User extends Anima configure_fields(:name, :age) do with_constructor allow_more: true, as_hash: true end end
Far better, more configurable. The problem with the first option is that it’s simply too opinated: it expects you to conform to what Anima expects you to use, overides your constructor (and you can’t un-override it – let’s imagine that you want to add Anima to some kind of library that expects you to call the superclass constructor. There is simply no way of doing it – at all (unless you use the even uglier way of picking up superclass instance method, bind it to you current class, and call it).
I think it all simplifies to the following: are you really sure that every other language out there, that don’t support metaprogramming the way Ruby supports, didn’t have this exactly same problem you’re trying to solve? So why you need to be “magical” about it? Even worse is when this kind of magical pattern blends into the logic – yes, I’m looking at you, Single Table Inheritance, and Polymorphic Associations. Did you ever try to optimize a query that uses polymorphic associations? I’ll give you a spoiler – there’s no way. You cannot join, you cannot preload in a simple query, you can’t even drop to pure SQL to do it. It’s impossible! Yet, every single project that I worked professionally with Ruby uses this aberration.
So, next time you think of a clever way of solving a problem, try to think on how would you solve it in other languages, or in pure SQL. If the answer is “there’s no way”, then put down your magic wand and rethink the problem. You’ll probably thank yourself in the future.