I once was hackathoning with a colleague who was trying to get me excited about Rails, and he said, "look how great this is -- if you want the idea of '1 day', you can just write `1.day`!". I opened up a irb to try it out, and it didn't work. We were both confused for a bit until he figured out that it was a Rails thing, not a Ruby thing. That Rails globally punches date/time methods into integers, which he thought was cool, and I thought was abhorrent. I asked, "okay, if I came across this code, how would I be able to know that it came from Rails?" He said, there wasn't any way to really trace a method to its source definition, you just kinda have to know, and I decided this whole thing was too much of a conflict with my mental model for how humans and code and computers should work together.
Look, I'm not a big fan of all of Rails' monkeypatching. That's why I don't use Rails anymore, I use other Ruby frameworks like Bridgetown, Roda, and Hanami. But there's definitely a way to dive into the "magic" and find out what's going on.
Even if you can see the source, it still seems difficult to understand where the monkeypatch came from if you have transitive dependencies and whatnot.
That way, it's easy to trace, forwards (from package to all the methods it introduces) & backwards (from method to package), who introduced a method, where, and why.
Other than that, I think a lot of this aversion to "ruby magic" is a bit overblown. The ability to cleanly remold any part of the system with minimal friction, to suit the app you're building right now - that's a KEY part of what makes it special.
Its like all these polemics warning wannabe lispers away from using macros. Lisp, Smalltalk, and ruby, all give you very powerful shotguns to express your creative ideas. If you can't stop blowing your own foot off, then pick a different language with a different paradigm.
> That way, it's easy to trace, forwards (from package to all the methods it introduces) & backwards (from method to package), who introduced a method, where, and why.
Doesn't Method#source_location in Ruby provide a mechanism for this?
Though, in modern (>3.x) Ruby, its probably better to avoid monkey-patching unless you need to override behavior exposed to consumers that aren't opting in, and just use Refinements that consumers can opt-in to using.
I can. It is, of course, those who worked on the project before me, that expressed their all-too-human over-confidence in their own abilities and judgement.
And then we would spend-a-weekend to rip-out the changes they'd made to Smalltalk system classes, that conflicted with the version upgrade.
(Something about the narcissism of small differences.)
plans = {
1.month => {standard: 10, pro: 50, enterprise: 100},
1.year => {standard: 100, pro: 500, enterprise: 1000}
}
plans.each do |interval, details|
details.each do |name, amount|
Billing::Plan::Factory.find_or_create_by!(name: , interval:, amount:)
end
endAlso ActiveSupport has Object#with_options which has a similar intent, but I rarely ever see it used in codebases.
This way of handling attributes is monumentally less efficient than just using keyword attributes, which are optimized by the runtime.
Unfortunately you’ll find this is every Ruby code base: tiny readability improvements that are performing allocations and wasting cycles for no real reason other than looking better.
I’ve certainly done that and it’s expected, efficient code looks “weird”. A regular “each” loop that looks complicated will be transformed into multiple array method chaining, allocating the same array many times. If you don’t do it someone else will.
Let's take this example from the article:
Billing::Plan.find_or_create_all_by_attrs!(
1.month => {standard: 10, pro: 50, enterprise: 100},
1.year => {standard: 100, pro: 500, enterprise: 1000}
)
This ensures six billing plans are created. That means 6 DB queries and 6 Stripe API queries, at a minimum.The problem with that logic is that it’s pervasive: people have that same attitude everywhere even if no IO is being done. That’s how we get multi gigabyte processes.
The whole language (and Rails) also pushes you towards a less efficient path. For instance you’re probably iterating over those six plans and inserting them individually in the DB. Another approach would’ve been to accumulate all of them in memory then build and perform a single query. That’s not something people really consider because it’s “micro” optimization and makes the code look worse. But if you miss out on hundreds of these micro optimizations then you get a worse system.
In a general sense optimizing Ruby is indeed futile: any optimization is dwarfed by just choosing a different language.
I say all this as someone who has worked with it for two decades, I like the language, it’s just laughably inefficient.
And after a couple years even Postgres is struggling because the amount of queries is too massive because of abstractions that don’t lend themselves to optimization.
Also it’s how you have codebases that could be maintained by two or three suddenly needing dozens because the testing suite needs hours to run and people even celebrate when there’s no tests in sight.
Just anecdotal personal experience. But I saw this happening inside at least 4 successful companies that started with Rails but didn’t care about those problems, and ended up wanting/having to move to something else.
Billing::Plan.find_or_create_all_by_attrs!(
standard => {1.month: 10, 1.year: 100},
pro => {1.month: 50, 1.year: 500},
enterprise => {1.month: 100, 1.year: 1000}
)Putting this kind of type-based 'magic' in the code is a bad decision that will bite you very soon. It optimizes for being 'cute' rather than being clear and maintainable, and that's a trade-off that almost never pays off.
Here's the example that runs in hundreds of integration tests:
expect(billing_pricing_plans).to eq billing_plans(
1.month => [:free, :premium, :pro, :enterprise],
1.year => [:free, :premium, :pro, :enterprise]
)
It asserts what plans the customers see on the pricing page.How would you then call the objects that store costs and billing frequency? :)
Here's what Stripe uses:
- Product: "describes the goods or services". This is where you define a (plan) name and features.
- Price: defines the amount, currency, and (optional) billing interval. Since interval is optional, Prices can be used to define both recurring, and one-off purchases.
Technically, using Prices for recurring, and one-off payments is a brilliant idea. The problem is, no one refers to recurring payments as "prices". Everyone calls a "$50 per year" option a "plan".
Billing::Plan::Factory.find_or_create_by!(
name: :pro,
interval: 1.month,
amount: 50
)
It is not only the verbosity or use of trailing '!' in a method
for no real reason, IMO, but also things such as "1.month". I
understand that rails thrives as a DSL, but to me having a method
such as .month on an Integer, is simply wrong. Same with
HashWithIndifferentAccess - I understand the point, to not have
to care whether a key is a String or a Symbol, but it is simply
the wrong way to think about this. People who use HashWithIndifferentAccess
do not understand Symbols.Ruby itself mostly uses it for mutating methods (e.g. #gsub("a", "b") replaces the character a with b in a string and returns a new string, but #gsub!("a", "b") mutates the original.
It's not that different from `1.times` or `90.chr` which are vanilla Ruby.
> HashWithIndifferentAccess
HashWithIndifferentAccess was an unfortunate necessity to avoid DOS attacks when Symbols used to be immortal. There's no longer a reason to use it today, except for backward compatibility.
Yes, "everything is an object" is an essential insight to understanding ruby.
The exclamation mark is a convention. It is used whenever a method could possibly result in an exception being raised. Sometimes it's instead used for non-idempotent methods.
"3.days", etc are Rails things. A lot of non-Rubyists don't like it but once you use it for long enough you tend to really grow to it.
As for HashWithIndifferentAccess, yes this is generally acknowledged as a mistake in Ruby's design and is rarely used in my experience. Originally, all Ruby hashes were HWIA. When they finally realized this was a design mistake they had to create HWIA for some level of backwards compatibility
I don't know its history well enough, but it seems to originate from Lisp. PG wrote about it before [1].
It can result in code that is extremely easy to read and reason about. It can also be incredibly messy. I have seen lots of examples of both over the years.
It is the polar opposite of Go's philosophy (be explicit & favour predictability across all codebases over expressiveness).
If there is a DSL such as Rails’ URL routing, which will be the same in every app—this is also fine.
When one makes 100s of micro-DSLs for object creation, that are only ever used in one or two places—this is pure madness.
But nobody forces you to use a DSL such as rails, so I am not sure why ruby should be hated for this when it is a rails dev who does that.
The blog has much more to do with rails than ruby; such API design is really strange.
I don't think this design causes problems as such, but it is too verbose and way too ugly. To me it seems that they are just shuffling data structures around; that could even be solved via yaml files.
```rb
[:red, 1.month, 10]
[:red, 1.year, 120]
[:blue, 1.month, 120]
[:blue, 1.year, 300]
```Every possible attribute (name, interval, amount) has at least two objects that share a value
If you want to make it even shorter, you have a few options - it really just comes down to preference:
# Option 1. my personal favorite, follows structure of
# intervals and plans on a pricing page.
1.month => {red: 10, blue: 120},
1.year => {red: 120, blue: 300}
# Option 2. this is fine too
red: {1.month => 10, 1.year => 120},
blue: {1.month => 120, 1.year => 300}
# Option 3. possible and works, but hurts my brain, NOT recommended
10 => {red: 1.month},
120 => {red: 1.year, blue: 1.month},
300 => {blue: 1.year}
> there is always at least one attribute that serves as a "discriminator" between the billing plans, rightJust a note: if you try to create two plans with the same attributes, that would error because of ActiveRecord uniqueness validations (and DB constraints). No point in having multiple identical plans.
Now every library, company or code base has its own pattern and you have to learn its pit falls. Better to learn once, cry once and just deal with it imo.
As they say, good enough is the enemy of perfection.
"Friendly Attributes" is not the "new way", not to be used "everywhere now", does not "apply to all scenarios".
If you like it, maybe you'll use it once in the next five years when the opportunity arises.
This is just using operator overloading to determine keywords, but it locks you out of ever using the same type twice in your signature. Notice that :usd turns into a name. What?
This is cute, but has no place in a professional software interface.
I don't see how you drew that conclusion. It seems to me the author provided several examples of this not being the case. Care to elucidate?
Or, as Patrick McKenzie used to tell us over and over, “charge more”.
(Yes, yes, I know some situations, customers, product, thinking, etc are different. But with broad brushstrokes, my advice is to not even entertain such a low price.)
If you are selling software, don't be the person charging $10/month. It's hard to make that business work.
Be the person charging $50/month. It's still hard - any business is - but it's much easier to make a software business financially viable if you charge decent money.
Which leads me to another piece of advice: don’t do B2C. Sell to businesses who will be far more willing to pay higher prices, will churn at a lower rate, and will - in general - require less support.