This is...not for me. It follows a big pattern in Ruby/Rails culture where to understand the code, you first have to understand the magic. And, it's never all that obvious where to go to try and understand the magic, because the magic itself has been imported by magic.
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.
1.method(:day).source_location <= your friend was wrong shrug
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.
That's one thing which ruby unfortunately did not adopt from Smalltalk. In Smalltalk (at least, in the dialects I'm familiar with), the "method categories" metadata is used to signal that we're adding new methods (or overwriting existing ones) to classes that are outside the scope of this package (ie: classes you didn't create as part of your app).
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.
I think this is the pattern I would reach for as well, separating the data from the execution. Being declarative about the plans (either with a config file, db backend, or simply a PORO) allows the plans themselves to be agnostic to how they are used and leaves you room to write a clean API for their creation without mixing in their definition.
Also ActiveSupport has Object#with_options which has a similar intent, but I rarely ever see it used in codebases.
Exactly. Use a fancy expressive structure if you want, but don't try to abstract away the mapping between that and the general-purpose code that it relies on. "Each domain has its own rules"? How would I even know where to look for those?
When I say Ruby is inefficient it’s not just the language, it’s stuff like this. I don’t fault the author but this kind of stuff is endemic.
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.
The usual response to this complaint in the Ruby/Rails community is that optimizing for nanoseconds, or even milliseconds doesn't matter when the same operation also involves multiple database queries or API calls.
This is a perfect example of something that looks good in a demo but fails in a real product. Business logic and 'packages' are never this clean or simple.
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.
Feels odd that two feature-equivalent plans are segregated with neighboring duplicates into monthly and yearly branches. I would consider monthly Enterprise & yearly Enterprise the same plan, with modified cost & billing frequency.
> I would consider monthly Enterprise & yearly Enterprise the same plan, with modified cost & billing frequency.
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".
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.
The exclamation mark has a reason: if the newly created records fails validations, an exception is raised. Without the exclamation mark, the error is silenced (and the method returns a falsey value). This is a convention across Rails.
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.
> I understand that rails thrives as a DSL, but to me having a method such as .month on an Integer, is simply wrong
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.
`1 + 1` in Ruby is syntactic sugar for `1.+(1)`. Nothing wrong about it at all, it's just different from what you're apparently used to. This type of thing isn't even unique to Ruby or even OOP.
This is a typical API design in Ruby, but the post is about a somewhat novel API design than what you're pointing out. To address some of your points:
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
Yikes. This means that you’ll have 1000 micro-DSLs sprinkled all over your codebase, which will become unreadable and lead to confusion/accidents. Better to stick with good ol’ key-value labelling.
This is a philosophy. One which many people that write Ruby subscribe to. The fundamental idea is: create a DSL that makes it very easy to implement your application. It is what made Rails different when it was created: it is a DSL that makes expressing web applications easy.
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).
Yeah, this is honestly the sort of thing I grew to hate in Ruby. It looks cute, but all it does is create more cruft. Good ol’ boring keys are just fine, expressive enough, and are very unlikely to cause problems. This feels like it’s attempting to solve a problem that does not exist.
So the underlying assumption is that there is always at least one attribute that serves as a "discriminator" between the billing plans, right? Is it possible to represent something like this then?
Your input would work exactly as you wrote it if passed to `Billing::Plan.find_or_create_all_by_attrs!`, just add commas at the end of lines.
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, right
Just 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.
Haters gonna hate. My take: DSLs are a useful way to make code easier to read, and more importantly easier to write correctly. Exploring this space and sharing your learnings is useful and valuable.
Ruby is a language that optimizes for the local maxima at the cost of the global maxima.
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.
Article author here - thank you for putting it this way. This is exactly the attitude I wanted to convey: it's something I tried and really liked for this specific use case. I shared because I hope it might inspire others.
"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.
I'll add another cautionary word in with everyone else who is panning this implementation.
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.
Off-topic, but unlike the example pricing plans, don’t make your SaaS’s “standard” plan $10/month. If you want a place to start, start with $50/month.
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.)
This is such a broad generalization as to be useless. I use several pieces of software that are around $10/month which there’s no way in hell I would pay $50 for.
montroser|3 months ago
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.
jaredcwhite|3 months ago
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.
isr|3 months ago
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.
drzel|3 months ago
bradly|3 months ago
Also ActiveSupport has Object#with_options which has a similar intent, but I rarely ever see it used in codebases.
Footkerchief|3 months ago
dlisboa|3 months ago
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.
brunosutic|3 months ago
Let's take this example from the article:
This ensures six billing plans are created. That means 6 DB queries and 6 Stripe API queries, at a minimum.kburman|3 months ago
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.
brunosutic|3 months ago
Here's the example that runs in hundreds of integration tests:
It asserts what plans the customers see on the pricing page.nkrisc|3 months ago
some1else|3 months ago
brunosutic|3 months ago
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".
shevy-java|3 months ago
Lukas_Skywalker|3 months ago
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.
byroot|3 months ago
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.
dagi3d|3 months ago
sodapopcan|3 months ago
culi|3 months ago
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
PufPufPuf|3 months ago
codesnik|3 months ago
dudeinjapan|3 months ago
molf|3 months ago
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).
[1]: https://paulgraham.com/progbot.html
rubyn00bie|3 months ago
culi|3 months ago
```rb
```Every possible attribute (name, interval, amount) has at least two objects that share a value
brunosutic|3 months ago
If you want to make it even shorter, you have a few options - it really just comes down to preference:
> 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.
sfgvvxsfccdd|3 months ago
AlphaSite|3 months ago
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.
brunosutic|3 months ago
"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.
culi|3 months ago
nvader|3 months ago
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.
culi|3 months ago
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?
stevoski|3 months ago
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.)
anamexis|3 months ago
PufPufPuf|3 months ago